---
title: Draft Orders Guide
description: 'Preview an order with a draft order, then place it'
sidebar:
  label: Guide
---

A **draft order** prices an order *without* placing it. It takes the same body as
[`POST /orders`](/api-reference/orders) and returns what that order would resolve to — the distribution center, fulfillment method, delivery
date, and payment method it would get, the alternatives available for each, per-line availability,
options, addons, and warranties, and the full pricing breakdown. A draft can then be placed by id,
so the order that ships is the one that was quoted.

## Benefits

- **Quote before you commit:** See the total, taxes, and fees before anything is placed
- **Discover the choices:** Every available fulfillment method, delivery date, payment method, addon, and warranty comes back resolved for the order in hand
- **Catch failures early:** Validation runs exactly as it does at placement, so a bad supplier, SKU, or option fails on the draft instead of on the order
- **Quote, then place it:** Place the draft by id and the order takes exactly the options and pricing the draft resolved — no body to reassemble and nothing to drift

## Core Concepts

**Draft Order**

A priced, validated preview of an order. It reserves nothing and charges nothing.

**Resolved Defaults**

The distribution center, fulfillment method, delivery date, and payment method the order would
get when the request leaves them unspecified.

**Availability**

Per-line stock status in the draft's distribution center: `IN_STOCK`, `OUT_OF_STOCK`,
`SUBSTITUTION_ONLY`, `UNAVAILABLE`, or `UNKNOWN`.

**Pricing**

Subtotal, tax, fees, and discounts, plus the `totalPriceCents` the order would charge.

## Creating a Draft Order

`POST https://api.sibipro.com/draft-orders` with a `create:draft-orders` scoped
[REST API token](/getting-started/rest-tokens). The body is the `POST /orders` body — at minimum a
`supplier`, an `address`, `contactInfo`, and one or more `lineItems`. Unlike `POST /orders`,
`paymentMethodId` is optional: leave it out and the draft resolves your default payment method and
lists every alternative in `availablePaymentMethods`, so a draft is how you discover which
`paymentMethodId` values you can place an order with.

```shell Curl
curl \
  --request POST \
  --header 'Authorization: Bearer <YOUR_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "supplier": "ge",
    "address": {
      "line1": "1630 W Guadalupe Rd",
      "city": "Gilbert",
      "stateOrProvince": "AZ",
      "postalCode": "85233"
    },
    "contactInfo": {
      "firstName": "John",
      "lastName": "Doe",
      "email": "john.doe@example.com",
      "phone": "1234567890"
    },
    "lineItems": [{ "sku": "GDF510PSRSS", "quantity": 1 }],
    "paymentMethodId": "MANUFACTURER_CREDIT"
  }' \
  https://api.sibipro.com/draft-orders
```

```javascript Fetch
const response = await fetch('https://api.sibipro.com/draft-orders', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${yourApiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    supplier: 'ge',
    address: {
      line1: '1630 W Guadalupe Rd',
      city: 'Gilbert',
      stateOrProvince: 'AZ',
      postalCode: '85233',
    },
    contactInfo: {
      firstName: 'John',
      lastName: 'Doe',
      email: 'john.doe@example.com',
      phone: '1234567890',
    },
    lineItems: [{ sku: 'GDF510PSRSS', quantity: 1 }],
    paymentMethodId: 'MANUFACTURER_CREDIT',
  }),
});
const draftOrder = await response.json();
```

A successful call returns `201` with the draft order:

```json Response
{
  "id": "01J9X0N3K7Q2VWY5B8ZC4TDR6H",
  "createdAt": "2026-08-31T00:00:00.000Z",
  "supplier": "ge",
  "address": {
    "line1": "1630 W Guadalupe Rd",
    "line2": null,
    "city": "Gilbert",
    "stateOrProvince": "AZ",
    "postalCode": "85233",
    "country": "USA"
  },
  "contactInfo": {
    "firstName": "John",
    "lastName": "Doe",
    "email": "john.doe@example.com",
    "phone": "1234567890"
  },
  "poNumber": null,
  "specialInstructions": null,
  "lineItems": [
    {
      "sku": "GDF510PSRSS",
      "name": "GE Dishwasher",
      "subtitle": "Front Control with Plastic Interior",
      "imageUrl": "https://images.sibipro.com/GDF510PSRSS.jpg",
      "quantity": 1,
      "unitOfMeasure": "Each",
      "quantityPerBaseUnit": 1,
      "baseUnitOfMeasure": "Each",
      "priceCents": 54900,
      "availabilityStatus": "IN_STOCK",
      "options": [],
      "attributes": [{ "name": "finish", "value": "Stainless Steel" }],
      "addons": [],
      "availableAddons": [{ "id": "haul-away", "title": "Haul Away", "priceCents": 2500 }],
      "availableWarranties": [
        {
          "sku": "GE-WARR-3YR",
          "name": "3 Year Protection Plan",
          "description": "Parts and labor for three years",
          "manufacturer": "GE",
          "priceCents": 9900
        }
      ],
      "selectedWarranty": null
    }
  ],
  "distributionCenter": {
    "id": "ge-dc-phx",
    "name": "Phoenix DC",
    "storeNumber": "0412",
    "phone": "6025550134",
    "address": {
      "line1": "4000 E Air Ln",
      "line2": null,
      "city": "Phoenix",
      "stateOrProvince": "AZ",
      "postalCode": "85034",
      "country": "USA"
    }
  },
  "fulfillmentMethod": {
    "id": "sibi-sibi-default-delivery",
    "title": "Standard Delivery",
    "description": null,
    "type": "DELIVERY",
    "availableDates": ["2026-09-01", "2026-09-02", "2026-09-03"]
  },
  "availableFulfillmentMethods": [
    {
      "id": "sibi-sibi-default-delivery",
      "title": "Standard Delivery",
      "description": null,
      "type": "DELIVERY",
      "availableDates": ["2026-09-01", "2026-09-02", "2026-09-03"]
    },
    {
      "id": "sibi-sibi-default-pickup",
      "title": "Will Call",
      "description": "Pick up at the distribution center",
      "type": "PICKUP",
      "availableDates": ["2026-09-01"]
    }
  ],
  "requestedDeliveryDate": "2026-09-01",
  "paymentMethodId": "MANUFACTURER_CREDIT",
  "availablePaymentMethods": [
    {
      "id": "MANUFACTURER_CREDIT",
      "type": "MANUFACTURER_CREDIT",
      "description": "On account",
      "last4": null
    }
  ],
  "pricing": {
    "subtotalCents": 54900,
    "taxCents": 4392,
    "totalPriceCents": 62292,
    "fees": [{ "name": "Delivery fee", "amountCents": 3000 }],
    "discounts": []
  },
  "subscribers": []
}
```

> **Note**
>
> `idempotencyKey` and `warrantyOrderPaymentMethodId` are accepted so the draft body and the order
> body stay interchangeable, but they only take effect at placement and are ignored here.

## Fetching a Draft

`GET https://api.sibipro.com/draft-orders/{id}` reads a draft back, with a `read:draft-orders`
scoped token. The response is the body `POST /draft-orders` returned, so a caller that drafts, reads
it back, then places it handles a single shape.

```shell Curl
curl \
  --request GET \
  --header 'Authorization: Bearer <YOUR_API_KEY>' \
  https://api.sibipro.com/draft-orders/<DRAFT_ORDER_ID>
```

```javascript Fetch
const response = await fetch(`https://api.sibipro.com/draft-orders/${draftOrderId}`, {
  headers: { Authorization: `Bearer ${yourApiKey}` },
});
const draftOrder = await response.json();
```

Nothing is repriced on read: the draft comes back as it was resolved, so it can describe
availability or pricing that has since changed and still be rejected at placement.

## Reading the Draft

The draft answers the questions you would otherwise have to guess at before placing the order.

| What you want to know | Where to look |
| --- | --- |
| What will this cost? | `pricing.totalPriceCents`, broken out into `subtotalCents`, `taxCents`, `fees`, and `discounts` |
| Is it in stock? | `lineItems[].availabilityStatus` |
| When can it arrive? | `fulfillmentMethod.availableDates`, or `availableFulfillmentMethods[].availableDates` for the other methods |
| How else can it be fulfilled? | `availableFulfillmentMethods` — each `id` is a `fulfillmentMethodId` |
| How can it be paid for? | `availablePaymentMethods` — each `id` is a `paymentMethodId` |
| Where would it ship from? | `distributionCenter` — its `id` is a `distributionCenterId` |
| What can be added to a line? | `lineItems[].availableAddons` (each `id` is an `addonIds` entry) and `lineItems[].availableWarranties` (each `sku` is a `warrantySku`) |

Prices are integer cents, dates are ISO 8601, and `quantityPerBaseUnit` × `quantity` gives the total
in `baseUnitOfMeasure` for products sold by area or length.

## Placing the Draft

`POST https://api.sibipro.com/draft-orders/{id}/order` places the order the draft describes,
with a `create:from-draft:orders` scoped token. The draft id in the path *is* the request — nothing
about the order can be changed here, so the order that ships is the one the draft priced.

The body is optional and carries only an `idempotencyKey`. Send one: resubmitting the same draft
with the same key within 5 minutes returns the order already placed instead of placing a second one.
A 5xx response or a timeout does not prove the order was not placed, so retry only with the same
key.

```shell Curl
curl \
  --request POST \
  --header 'Authorization: Bearer <YOUR_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{"idempotencyKey": "b8f0e6d2-1c3a-4f5e-9a7b-0d2c4e6f8a1b"}' \
  https://api.sibipro.com/draft-orders/<DRAFT_ORDER_ID>/order
```

```javascript Fetch
const response = await fetch(`https://api.sibipro.com/draft-orders/${draftOrder.id}/order`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${yourApiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ idempotencyKey }),
});
const order = await response.json();
```

A successful call returns `201` with the same body `POST /orders` returns, so a caller that
drafts-then-places handles one response shape:

```json Response
{
  "id": "SIBI-12345678",
  "orderUrl": "https://web.sibipro.com/orders/SIBI-12345678",
  "warrantyOrderId": null,
  "warrantyOrderUrl": null
}
```

### Changing an order before placing it

A draft is fixed. To order something different — another fulfillment method, delivery date, payment
method, distribution center, addon, or warranty — copy the chosen identifier out of the draft into
the same body you drafted with and create a **new** draft, then place that one:

1. **Draft**

    Post the order body to `/draft-orders` and read the resolved defaults and alternatives

2. **Redraft to change**

    To change anything, copy the chosen `fulfillmentMethodId`, `requestedDeliveryDate`, `paymentMethodId`, `distributionCenterId`, `addonIds`, or `warrantySku` into the body and draft again

3. **Place**

    Post to `/draft-orders/{id}/order` with an `idempotencyKey`

A draft becomes exactly one order. Posting to a draft that already has one is refused with `409
DRAFT_ORDER_ALREADY_PLACED`, and the message names the order it became so a caller that lost the
response can pick that order up instead of retrying.

> **Warning**
>
> A draft order reserves nothing. Pricing and availability are accurate as of the moment the draft
> was created, and placement re-validates against live data — a draft that priced cleanly can still
> be rejected if availability, a fulfillment date, or a payment method has changed since. Draft
> immediately before placing rather than reusing an old draft.

> **Note**
>
> `POST /orders` remains available for placing an order in one call, without drafting first. It
> takes the same body and requires the `create:orders` scope.

## Errors

Validation runs on the draft exactly as it does at placement, so both endpoints return the same
`400` codes as `POST /orders` — `VALIDATION_ERROR`, `INVALID_SUPPLIER`, `PRODUCTS_NOT_FOUND`,
`INVALID_PRODUCT_OPTIONS`, `NO_AVAILABLE_DELIVERY_METHODS`, and the rest of the list on that
operation. Authentication and scope failures follow the usual codes; see
[Making a REST Request](/rest/making-a-rest-request). Every draft-orders endpoint requires a REST API
token: a token issued for the GraphQL API receives a `403` with code `REST_TOKEN_REQUIRED`.

```json Error
{
  "code": "PRODUCTS_NOT_FOUND",
  "message": "The following products were not found: not-a-real-sku"
}
```

Addressing a draft by id adds two more codes:

| Status | Code | Meaning | Where |
| --- | --- | --- | --- |
| 404 | `DRAFT_ORDER_NOT_FOUND` | No draft order with this id is visible to the caller | Fetching and placing |
| 409 | `DRAFT_ORDER_ALREADY_PLACED` | This draft has already been placed; the message names the order it became | Placing |

An id that never existed and one belonging to another organization answer identically, so neither
endpoint can be used to discover which draft ids exist.

```json Error
{
  "code": "DRAFT_ORDER_ALREADY_PLACED",
  "message": "This draft order has already been placed as order SIBI-12345678"
}
```

## Testing

The [test environment](/rest/test-environment) serves the same endpoints under
`https://dev.sibi.pro/draft-orders*`, so a draft is created with
`POST https://dev.sibi.pro/draft-orders`, read back with
`GET https://dev.sibi.pro/draft-orders/{id}`, and placed with
`POST https://dev.sibi.pro/draft-orders/{id}/order`. Mint a **test** REST API token for it — a
production token sent to the test host, or a test token sent to `https://api.sibipro.com`, is
refused with `403 TOKEN_ENVIRONMENT_MISMATCH`. Drafts priced there price against test data, and
orders placed there trigger no real shipment or charge.

## Endpoints

**[Draft Orders endpoints](/api-reference/draft-orders)**

Every endpoint with parameters, response schemas, and code samples.
