Draft Orders Guide
Preview an order with a draft order, then place it
A draft order prices an order without placing it. It takes the same body as
POST /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. The body is the POST /orders body — at minimum a
supplier, an address, contactInfo, one or more lineItems, and a paymentMethodId.
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
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:
{
"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",
"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": []
}
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.
curl \
--request GET \
--header 'Authorization: Bearer <YOUR_API_KEY>' \
https://api.sibipro.com/draft-orders/<DRAFT_ORDER_ID>
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.
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
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:
{
"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:
Draft
/draft-orders and read the resolved defaults and alternativesRedraft to change
fulfillmentMethodId, requestedDeliveryDate, paymentMethodId, distributionCenterId, addonIds, or warrantySku into the body and draft againPlace
/draft-orders/{id}/order with an idempotencyKeyA 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.
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. Every draft-orders endpoint requires a REST API
token: a token issued for the GraphQL API receives a 403 with code REST_TOKEN_REQUIRED.
{
"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.
{
"code": "DRAFT_ORDER_ALREADY_PLACED",
"message": "This draft order has already been placed as order SIBI-12345678"
}
Testing
The 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
Every endpoint with parameters, response schemas, and code samples.