Docs · API

API and webhooks

Create a quote from a part's files and read the price at each quantity from your own website, customer portal or ERP, then get a signed webhook when your buyer accepts it with a PO. The API prices exactly as the quote screen does, with your shop's rates, materials and markup, and every quote it makes is in your QuotedPart account to review and send.

Start

  1. In QuotedPart, an owner opens Integrations → API keys, names a key after what will use it (Website, ERP) and copies it. It's shown once.
  2. Send it with every request as Authorization: Bearer qpk_…. Base URL: https://quotedpart.com/api/v1.
  3. Check it: curl https://quotedpart.com/api/v1/shop -H "Authorization: Bearer $QUOTEDPART_API_KEY" answers your shop's name, currency, RFQ page and RFQ email address.
Keys
Made and revoked by shop owners in Integrations → API keys; only a hash is stored. A key acts as the owner who made it and stops when it's revoked or that person can no longer edit quotes. Keep it on your server: the API refuses calls from web pages, and anyone with the key can quote in your shop.
Limits
Per key and hour: 100 new quotes and 2,000 other requests; over a limit the answer is 429 with Retry-After. A request takes files up to about 4 MB in total; send larger files as URLs (up to 40 MB each).
Errors
JSON { "error": { "code": "…", "message": "…" } } with status 400 (a field is wrong; the message says which), 401 (the key), 404, 413 (too large), 415, 429 or 5xx.
Drawings
PDF drawings are read by AI, usually in 20 to 60 seconds, within your shop's AI allowance. A model alone prices in seconds.

Create a quote

POST /api/v1/quotes with multipart/form-data (the files as file fields, any field name) or JSON (each file as url or content_base64 with its name). A model and a PDF with the same name are one part; every other file is a part of its own, up to 25.

files
STEP (.step, .stp), IGES (.iges, .igs), DXF flat patterns (.dxf) and PDF drawings. Required.
quantities
Up to 8 quantities for every part, like "10,50,100". Default: your shop's quantity breaks, or the drawing's quantity.
customer
company, name, email (a JSON object, or the form fields customer_company, customer_name, customer_email). Matched to your customers by email, else added.
due_date
When the buyer wants the quote, yyyy-mm-dd.
needed_by
When the buyer needs the parts, yyyy-mm-dd.
notes
The buyer's requirements as text (finish, certifications, delivery). Shown on the quote as the customer's message; they don't change the price by themselves.
material
A material for every part, by name as you'd write it ("6061", "304 stainless"), matched to your shop's materials. Default: the drawing's material, else your default for the process.
process
cnc_mill, cnc_turn, sheet_laser or additive, for every part. Default: chosen from the model.
order_type
prototype, production or repeat: for your markup rules (rfq.is_prototype and the others), as from an email.
rush, turnaround_days
true when it's a rush; the turnaround asked for in working days (rfq.rush, rfq.turnaround_days).
title
The quote's title. Default: the part's name.
wait
Seconds to wait for drawing reads before answering, 0 to 40 (default 20). If a read isn't done, pricing is "reading": get the quote again.
multipart
curl https://quotedpart.com/api/v1/quotes \
  -H "Authorization: Bearer $QUOTEDPART_API_KEY" \
  -F "files=@NW-1042 mounting bracket.step" \
  -F "files=@NW-1042 mounting bracket.pdf" \
  -F "quantities=10,50,100" \
  -F "customer_company=Acme Robotics" \
  -F "customer_name=Jordan Lee" \
  -F "customer_email=jordan@acme-robotics.example" \
  -F "needed_by=2026-12-15" \
  -F "notes=Black anodize Type II. Material certs with shipment."
JSON
curl https://quotedpart.com/api/v1/quotes \
  -H "Authorization: Bearer $QUOTEDPART_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      { "name": "NW-1042 mounting bracket.step", "url": "https://files.example.com/rfq-4471/NW-1042.step" },
      { "name": "NW-1042 mounting bracket.pdf", "content_base64": "JVBERi0xLjcK..." }
    ],
    "quantities": [10, 50, 100],
    "customer": { "company": "Acme Robotics", "name": "Jordan Lee", "email": "jordan@acme-robotics.example" },
    "due_date": "2026-11-06",
    "needed_by": "2026-12-15",
    "notes": "Black anodize Type II. Material certs with shipment.",
    "wait": 30
  }'

The answer is 201 with the quote (below) and a Location header. The quote is a draft in your account, its internal notes saying which key made it, for your team to review and send; nothing goes to the buyer by itself.

Get a quote

GET /api/v1/quotes/{id}, by its id or its number (Q-1042). Add ?wait=20 to wait up to that many seconds for drawing reads to finish.

The quote (example)
{
  "id": "k57c2x0n8w3h1q9b6e4r5t2y7u8i0o3p",
  "number": "Q-1042",
  "title": "NW-1042 mounting bracket",
  "status": "draft",
  "pricing": "priced",
  "currency": "USD",
  "total": 1236.4,
  "customer": { "company": "Acme Robotics", "name": "Jordan Lee", "email": "jordan@acme-robotics.example" },
  "due_date": "2026-11-06",
  "needed_by": "2026-12-15",
  "notes": "Black anodize Type II. Material certs with shipment.",
  "created_at": "2026-10-29T15:02:11.000Z",
  "updated_at": "2026-10-29T15:02:40.000Z",
  "app_url": "https://quotedpart.com/quotes/k57c2x0n8w3h1q9b6e4r5t2y7u8i0o3p",
  "customer_url": null,
  "parts": [
    {
      "id": "j97f…",
      "name": "NW-1042 mounting bracket",
      "part_number": "NW-1042",
      "revision": "B",
      "status": "priced",
      "error": null,
      "files": { "model": "NW-1042 mounting bracket.step", "drawing": "NW-1042 mounting bracket.pdf" },
      "process": "CNC milling",
      "material": "Aluminum 6061-T6",
      "finishes": ["Anodize Type II, black"],
      "selected_quantity": 10,
      "quantities": [
        { "quantity": 10, "unit_price": 123.64, "total": 1236.4, "lead_days": 12 },
        { "quantity": 50, "unit_price": 61.2, "total": 3060, "lead_days": 15 },
        { "quantity": 100, "unit_price": 48.75, "total": 4875, "lead_days": 18 }
      ],
      "checks": []
    }
  ],
  "charges": []
}
pricing
priced: every part has a price. reading: a model or drawing is still being read. needs_input: a part needs a person (a size the drawing doesn't give, for example). failed: a file couldn't be read.
parts[].quantities
Unit price, line total (with the line's own charges, such as tooling) and lead time in days at each quantity. Prices only: never cost, rates or margin.
parts[].checks
What an estimator should look at before sending, in plain words: a tight tolerance, a stand-in material, a price outside the usual range.
status
The quote's own state: new or draft until your team sends it, then sent, won, lost, expired or no_bid.
app_url
The quote in your account.
customer_url
The buyer's page, with your shop's name and logo, where they accept with a PO; null until the quote is sent.

List quotes

GET /api/v1/quotes: your shop's quotes, most recently changed first, without their parts. Parameters: limit (1 to 100, default 20), status (for example sent), customer_email (one customer's quotes, for their page in your portal) and before, the next_before of the previous page.

Webhooks

In Integrations → Webhook, an owner enters an https address on your server. QuotedPart POSTs a JSON event there, signed with the webhook's secret (Integrations → Webhook → Show); Send test event sends a ping.

quote.accepted
Your buyer accepted a quote on its page with a PO number or PO file (or paid by card): the order's lines, quantities, prices, PO and ship-to.
rfq.received
An RFQ arrived from your RFQ page, your website form or your RFQ email address: the new quote, its parts and the buyer as far as known. For an email, the buyer's details are read a few seconds later; get the quote for them.
ping
Send test event.
quote.accepted
{
  "id": "evt_3kq9…",
  "type": "quote.accepted",
  "created": 1793300000,
  "data": {
    "quote": { "id": "k57c…", "number": "Q-1042", "title": "NW-1042 mounting bracket",
               "app_url": "https://quotedpart.com/quotes/k57c…", "customer_url": "https://quotedpart.com/q/…" },
    "customer": { "company": "Acme Robotics", "contact": "Jordan Lee", "email": "jordan@acme-robotics.example" },
    "order": {
      "id": "m12…", "buyer_name": "Jordan Lee", "buyer_email": "jordan@acme-robotics.example",
      "po_number": "PO-88213", "po_file_url": "https://…", "ship_to": "…", "ship_date": "2026-12-15T00:00:00.000Z",
      "payment": "po", "currency": "USD",
      "lines": [{ "part_id": "j97f…", "name": "NW-1042 mounting bracket", "quantity": 50, "unit_price": 61.2, "total": 3060, "lead_days": 15 }],
      "charges": [], "total": 3060
    }
  }
}

Verify the signature

Each request carries QuotedPart-Signature: t=1793300000,v1=5f2c…: v1 is the hex HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the secret. Refuse a signature that doesn't match or is older than five minutes. QuotedPart-Event names the event; QuotedPart-Delivery stays the same on every retry of one event.

Node.js
import crypto from "node:crypto";

// raw: the request body exactly as received (a string); header: the QuotedPart-Signature header
function verify(raw, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes: refuse
  const want = crypto.createHmac("sha256", secret).update(`${t}.${raw}`).digest("hex");
  return parts.v1?.length === want.length && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1));
}
PHP
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_QUOTEDPART_SIGNATURE']), $sig);
$want = hash_hmac('sha256', $sig['t'] . '.' . $raw, getenv('QUOTEDPART_WEBHOOK_SECRET'));
if (abs(time() - (int)$sig['t']) > 300 || !hash_equals($want, $sig['v1'])) { http_response_code(400); exit; }
$event = json_decode($raw, true);

Answer with any 2xx within 10 seconds. Anything else, or no answer, is tried again after 1, 5, 30, 120 and 360 minutes and a day; redirects aren't followed. The last ten deliveries and their results are listed under the webhook.