---
title: "Create quotes through the API"
description: "Create quote requests over the QuotWay API from a headless storefront or an ERP - lines, buyer, custom fields, eligibility and idempotency."
url: "https://www.quotway.com/fr/docs/api/create-quotes"
type: "documentation"
category: "api"
updated: "2026-09-30"
locale: "fr"
source: "QuotWay - B2B Quote & Negotiation App for Shopify"
---

# 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_quotes` scope.

## 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.created` [webhook](/docs/api/webhooks) fires with `data.event.source` = `"api"`.

## Request

```bash
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](#customers-and-b2b-companies). |
| `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](#lines). |
| `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:

```json
{ "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**; otherwise `400 invalid_request` on `lines.<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:

```json
{ "custom": { "title": "Custom logo print setup", "description": "…" }, "quantity": 1 }
```

- `custom.title` is required (max 255); `custom.description` is 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_price` is 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_id` only 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](/docs/api/quote-object), in status `submitted`:

```json
{
  "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`](/docs/api/errors#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`](/docs/api/errors#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`](/docs/api/errors#invalid_custom_fields) | A required custom field is missing or a value is invalid. |
| 409 | [`quote_creation_paused`](/docs/api/errors#quote_creation_paused) | The store's subscription is frozen. |
| 429 | [`email_quota_exceeded`](/docs/api/errors#email_quota_exceeded) | The store's daily allowance of buyer-emailing API calls is used up. See [Rate limits](/docs/api/rate-limits#is-there-a-limit-on-emails-to-buyers). |
| 409 / 422 | [`idempotency_key_in_use`](/docs/api/errors#idempotency_key_in_use) / [`idempotency_key_reused`](/docs/api/errors#idempotency_key_reused) | See [Idempotency](/docs/api/idempotency). |

## Automation rules run on API quotes too

The new quote is handed to the store's [automation rules](/docs/automation/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](/docs/api/idempotency).

## Related

- [Messages and proposals](/docs/api/messages-and-proposals) - the next steps on the quote.
- [Targeting rules](/docs/targeting/targeting-rules) - what decides `not_eligible`.
- [Custom fields and conditional logic](/docs/quote-form/custom-fields-and-conditional-logic)
