Create quotes through the API
Read time: 8 minutes. Who it's for: Developers building a headless or custom storefront, an agency integration, or an iPaaS flow that captures quote requests outside the Online Store.
⚠️ Requires the Enterprise plan and a key with the
write_quotesscope.
What does POST /v1/quotes do?
It records a buyer's quote request - the same thing that happens when a buyer presses
"Request a quote" on your Online Store. It is not a way to create a priced quote: the request lands
in QuotWay with status submitted, and your team prices and sends the proposal as usual (or your
automation rules do).
Because it goes through the same intake as the storefront, an API request can't behave differently from a storefront one:
- your targeting rules decide whether the request is allowed;
- your quote form's custom fields are validated;
- your automation rules run on the new quote;
- your staff get the usual "New quote received" email (if it's switched on in Settings);
- a
quote.createdwebhook fires withdata.event.source="api".
Request
curl https://api.quotway.com/v1/quotes \
-H "Authorization: Bearer $QUOTWAY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9d3e1f7a-5b2c-4e8d-a1f0-6c7b8a9d0e12" \
-d '{
"buyer": {
"email": "buyer@example.com",
"name": "Dana Ortiz",
"phone": "+1 303 555 0142",
"company_name": "Acme Restaurant Group"
},
"customer_id": "7012345678",
"lines": [
{ "variant_id": "gid://shopify/ProductVariant/4455667788", "quantity": 100, "requested_price": "11.50", "note": "Need the compostable version" },
{ "custom": { "title": "Custom logo print setup", "description": "Two-colour print on the cup sleeve" }, "quantity": 1 }
],
"notes": "Delivery to our Denver kitchen, please.",
"po_number": "PO-88213",
"requested_delivery_date": "2026-11-01",
"shipping_address": {
"first_name": "Dana", "last_name": "Ortiz", "company": "Acme Restaurant Group",
"address1": "1200 Larimer St", "city": "Denver", "province_code": "CO", "country_code": "US", "zip": "80204"
},
"custom_fields": { "industry": "Hospitality" },
"source_url": "https://shop.example.com/products/compostable-cup"
}'
Body fields
| Field | Required | Description |
|---|---|---|
buyer.email |
Yes | The buyer's email (max 254). Stored lowercase. |
buyer.name |
No | Max 200 characters. |
buyer.phone |
No | Max 40 characters. |
buyer.company_name |
No | Max 200 characters. |
customer_id |
No | A Shopify customer of this store - GID (gid://shopify/Customer/7012345678) or numeric id. Verified with Shopify. See Customers and B2B. |
company_location_id |
No | Preferred Shopify company location (GID or numeric) when the customer belongs to several. Ignored unless the customer is really a member. |
lines |
Yes | 1–100 lines. Each is a variant line or a custom line. |
notes |
No | The buyer's note on the request (max 5,000). |
po_number |
No | Purchase-order number (max 100). |
requested_delivery_date |
No | A date, e.g. 2026-11-01. |
shipping_address, billing_address |
No | Objects with any of first_name, last_name, company, address1, address2, city, province_code, country_code (2 letters), zip, phone. |
custom_fields |
No | Answers to the store's custom quote-form fields, keyed by field key. Values are a string (max 2,000), a boolean, or an array of strings. |
source_url |
No | The page the request came from (max 2,000), for your team's reference. |
Request bodies are strict. Any key not listed above - for example a price on a line - is
rejected with 400 invalid_request, never silently ignored.
Lines
A variant line names a Shopify product variant:
{ "variant_id": "gid://shopify/ProductVariant/4455667788", "quantity": 100, "requested_price": "11.50", "note": "…" }
variant_id- GID or numeric id. The variant must exist in this store and its product must be active; otherwise400 invalid_requestonlines.<n>.variant_id.- QuotWay reads the title, variant title, SKU, image, catalog price, product type, vendor, tags and collections from Shopify. Nothing you send about the product is stored.
A custom line is for something that isn't a product in the store:
{ "custom": { "title": "Custom logo print setup", "description": "…" }, "quantity": 1 }
custom.titleis required (max 255);custom.descriptionis optional (max 2,000).
Both kinds take:
quantity- an integer from 1 to 1,000,000;requested_price- optional; the buyer's target unit price as a decimal string ("11.50"), shown to your team next to the catalog price;note- optional; the buyer's note on the line (max 1,000).
What can't I set?
The API records what the buyer asked for. It never lets you set:
- prices - the catalog price comes from Shopify and the offered price is set by your team when
they build the proposal (
requested_priceis only the buyer's target); - the quote's status, assignee, source or currency;
- product details such as title or SKU.
Prices are in the store's base currency. Market or presentment-currency pricing isn't applied to API requests.
Customers and B2B companies
Without customer_id, the request is treated as coming from a guest buyer identified by email.
With customer_id, QuotWay checks with Shopify that the customer exists in the store (otherwise
400 invalid_request on customer_id), and then:
- uses the customer's real Shopify tags for customer-based targeting rules;
- looks up the customer's real B2B company memberships. If the customer is a contact of a
Shopify B2B company location, the quote becomes company-aware (
mode: "b2b") and carries that company and location - exactly as it would for a buyer logged in on your storefront.company_location_idonly chooses between locations the customer genuinely belongs to; it can't attach a quote to a company the customer isn't part of.
Otherwise the quote is a standard wholesale quote (mode: "wholesale").
Response
201 Created with the new quote, in status submitted:
{
"object": "quote",
"id": "clz9a7b6c0001qw7hm3n4o5p6",
"number": "QW-1057",
"status": "submitted",
"mode": "wholesale",
"source": "api",
"currency": "USD",
"version_number": 0,
"…": "…",
"totals": {
"currency": "USD",
"subtotal": "1400.00",
"total": "1400.00",
"headline": "1400.00",
"headline_mode": "none",
"…": "…"
},
"lines": [
{
"id": "clz9a7b6d0002qw7hq7r8s9t0",
"quote_line_item_id": null,
"position": 0,
"variant_id": "gid://shopify/ProductVariant/4455667788",
"title": "Compostable cup, 12 oz",
"quantity": 100,
"catalog_price": "14.00",
"requested_price": "11.50",
"unit_price": null,
"line_total": "1400.00",
"is_custom": false,
"decision": null,
"accepted": false,
"still_open": true,
"…": "…"
}
],
"proposal": null
}
Until a proposal is sent, the quote's figures are an estimate at catalog price: each variant
line's line_total is catalog_price × quantity, unit_price is null, custom lines have no
price, and totals.subtotal/total are the catalog sum.
Errors you should handle
| Status | Code | When |
|---|---|---|
| 400 | invalid_request |
A field is invalid or unknown; a variant or customer isn't in the store; a product isn't active. |
| 422 | not_eligible |
The store's targeting rules don't allow the request. reason is NO_MATCHING_PRODUCT_RULE (with line_index) or NO_MATCHING_CUSTOMER_RULE. |
| 422 | invalid_custom_fields |
A required custom field is missing or a value is invalid. |
| 409 | quote_creation_paused |
The store's subscription is frozen. |
| 429 | email_quota_exceeded |
The store's daily allowance of buyer-emailing API calls is used up. See Rate limits. |
| 409 / 422 | idempotency_key_in_use / idempotency_key_reused |
See Idempotency. |
Automation rules run on API quotes too
The new quote is handed to the store's automation rules the
moment it's created, in the background. So the 201 response shows submitted, but within moments
a rule can act on it - an auto-decline rule can decline it, an auto-send rule can send a
proposal, an auto-assign rule can assign it. Don't assume a quote you just created is still
submitted: subscribe to quote.declined, quote.proposal_sent and quote.assigned, or fetch the
quote again before acting on its status.
Retry safely
Always send an Idempotency-Key. If the connection drops after QuotWay created the quote, the
retry with the same key returns the same 201 response instead of creating a duplicate. Use a new
key for every new request. See Idempotency.
Related
- Messages and proposals - the next steps on the quote.
- Targeting rules - what decides
not_eligible. - Custom fields and conditional logic
Besoin d'un coup de main ? L'équipe est là pour vous aider.