Orders
Preorder order reads, the dashboard's order actions, and bulk actions.
BetaAPI v2 is in public beta: endpoints, fields and behaviour can change before general availability. See the changelog
Last updated
The orders resource is the runtime counterpart to Offer. The offer resource configures what preorder behavior looks like; the orders resource reads individual customer orders and runs the order actions the merchant dashboard has: release, open, hold, split, reschedule fulfillment or payment, collect payment, and resend the confirmation email, SMS or a shipping update. Each action runs the same step the dashboard runs.
There is no API to cancel, refund or edit tags on an order, because the dashboard has no such action.
Path prefix: /preorders/orders
Our id is always id (or <thing>_id). A Shopify id is always shopify_<thing>_id and is a gid string, e.g. gid://shopify/Order/6123456789012.
- The reads (
GET /preorders/orders/:idand its section URLs) take any order reference; see Looking up an order. - Actions (
POST /preorders/orders/:id/...) and bulkorder_idstake only our id or a Shopify order id / gid, so they never act on a guess. Anything else returns404(single) or422(bulk).
When an action is refused
Section titled “When an action is refused”Actions check the order first and return 409 Conflict with the reason when it doesn’t allow them:
| Action | Refused when the order is |
|---|---|
release |
cancelled or fulfilled |
collect_payment |
cancelled |
| Method | Path | Description |
|---|---|---|
GET |
/preorders/orders |
List preorder orders in this shop, paginated. |
GET |
/preorders/orders/:id |
Read an order’s full representation. |
GET |
/preorders/orders/:id/payments |
The payments block of the full read. |
GET |
/preorders/orders/:id/fulfillment |
The fulfillment block of the full read. |
GET |
/preorders/orders/:id/notifications |
The notifications block of the full read. |
GET |
/preorders/orders/:id/timeline |
The order’s activity log, newest first. Not in the full read. |
Tags are in the full read only. There is no separate tags read.
Looking up an order
Section titled “Looking up an order”:id in the reads can be:
- STOQ’s order id (
idin every response), which also reaches orders not yet synced from Shopify - the Shopify order id, or
gid://shopify/Order/… - the order name or number:
#1234(URL-encoded as%231234) or1234 - the customer’s email or name
- a Shopify admin order URL
A bare number is read as a Shopify order id when one matches, else as an order number. When a reference matches several orders, the read returns 200 with { "ambiguous": true, "query": "…", "candidates": [{ "id", "shopify_order_id", "name", "type", "customer_email", "customer_name", "created_at" }] }; read the one you want by its id. No match, or an order in another shop, is 404.
The full read and every section URL also read back-in-stock orders; type is preorder or back_in_stock.
List filters
Section titled “List filters”The filters are the ones on Reports > Preorders > Orders and run the dashboard’s own query. They combine with AND.
| Param | Notes |
|---|---|
search |
Order number or name, customer name, email, a Shopify order id, an order gid, or an admin order URL |
financial_status, fulfillment_status |
Shopify values, comma-separated |
preorder_fulfillment_status |
on_hold, scheduled, open, comma-separated |
cancelled_status |
cancelled or not_cancelled (one value) |
payment_collection_status |
Latest collection attempt: failed, success, unknown, comma-separated |
offer_id |
Our offer ids, comma-separated. Active, inactive and deleted offers, like the dashboard’s three offer filters |
shopify_variant_id |
Orders containing these variants. gid://shopify/ProductVariant/<id> gids, comma-separated |
shopify_product_id |
Orders containing these products’ variants, like the dashboard’s Product filter. gid://shopify/Product/<id> gids, comma-separated |
vendors |
Vendor names (vendors[]=Acme&vendors[]=Bolt; a single vendors=Acme is one name). Matches the vendor of the order’s preorder items only |
customer_id |
Our customer id (customer.id) |
from, to |
Created-at window, inclusive. A bare date (2026-03-10) is that whole day in the shop’s time zone, like the dashboard’s Date range; a time is read as given |
page, per_page |
Pagination. Default per_page is 50, max 200 |
An unknown filter value, an unknown offer or an id that is not the right kind of gid returns 422 {errors: {"<filter>": ["..."]}}.
The response key is orders, plus a meta pagination block:
{ "orders": [ { "id": "d608978f-9707-4ccf-b917-bd97ab103619", "shopify_order_id": "gid://shopify/Order/6123456789012", "name": "#1042", "...": "..." } ], "meta": { "total_count": 47, "page": 1, "per_page": 50, "total_pages": 1 }}Each list entry has the read’s top-level fields (no sections) plus the dashboard row’s Products and Offers linked columns:
products: the order’s preorder lines,[{ name, shopify_variant_id }].nameis the line’s title and variant title, as the row shows it.linked_offers:[{ offer_id, offer_internal_name, deleted }].deletedistruefor an offer that has since been deleted, which the row marks.
Back-in-stock orders list
Section titled “Back-in-stock orders list”GET /back_in_stock/orders lists the orders on Reports > Back in stock > Sales recovered (medium and high confidence attributions), with the same orders and meta keys. Each order has id (ours), type, shopify_order_id (gid), name, customer ({ id, email, name }), total_price, currency, financial_status, fulfillment_status, notified_product, notification and created_at.
notified_product: the Product column, the variant the alert was for:{ shopify_product_id, shopify_variant_id, title }(gids).nullwhen the order has no alert linked.notification: the Type and Notification sent at columns:{ channel, sent_at }.channelisemailorsms, orpushfor an old web push send (STOQ no longer sends web push).nullwhen no message is linked.
| Param | Notes |
|---|---|
search |
As above |
product_match_type |
same_as_notification: only orders where the customer bought the exact variant they were alerted about (the dashboard’s Order matching: Exact variant match only) |
shopify_variant_id, shopify_product_id |
One gid each. Orders containing that variant or product, like a product’s or variant’s Orders tab |
customer_id |
Our customer id |
from, to |
As above |
page, per_page |
As above |
Read shape
Section titled “Read shape”| Field | Type | Notes |
|---|---|---|
id |
uuid | Our order id |
type |
string | preorder or back_in_stock |
shopify_order_id |
gid | null until Shopify syncs the order |
name |
string | Shopify order name, e.g. #1042 |
customer |
object | { id, shopify_customer_id, email, phone, name }. id is ours, shopify_customer_id is a gid |
total_price, currency |
money, string | |
financial_status, fulfillment_status |
string | As Shopify reports them |
payment_status |
string | not_synced, paid, paid_in_full, balance_pending, balance_overdue, balance_collected, cancelled, or the refund status |
cancelled_at |
iso8601 | When Shopify cancelled the order; null if it isn’t cancelled |
payments |
object | subtotal_price (Shopify’s current subtotal; null until synced), tax_lines ([{ title, rate, price }], rate a fraction such as 0.2), amounts paid and outstanding, due_at, overdue, balance_collected_at, last_payment_attempt and payment_attempts. Only synced or recorded values; null when unknown |
fulfillment |
object | fulfillment_status, preorder_fulfillment_status, hold_reason_note, holds_applied_at, shopify_split_order_ids (gids), offers ([{ offer_id, handling }]) |
notifications |
object | Per kind (confirmation_email, confirmation_sms, shipping_update_email): sent_count, last_sent_at, last_triggered_by |
tags |
object | tags is read live from Shopify (null if it can’t be read); configured_by_offers is what the offers apply |
attributed_offers |
array | One entry per offer; see below |
line_items |
array | id (pass it as a line_item_ids entry), line_total (price times quantity plus tax; a bundle’s is its components’), shopify_line_item_id, shopify_line_item_group_id, shopify_variant_id (gids), title, variant_title, fulfillment_status, preorder_fulfillment_status (that line’s on_hold, scheduled, open or null), is_preorder, offer_id, quantity |
created_at, updated_at |
iso8601 |
fulfillment.offers[].handling is none, hold_all, split_fulfillments or separate_orders.
Each attributed_offers entry has offer_id, offer_name, offer_internal_name, current_status, shipping, shopify_line_item_ids and attribution_url. shipping is the offer’s shipping section exactly as GET /preorders/offers/:id/shipping returns it: same keys, names and values.
A native Shopify bundle never appears as a Shopify line item; only its components do. When the bundle is on a preorder offer, line_items has one entry for the bundle (shopify_line_item_id: null, shopify_line_item_group_id set, e.g. gid://shopify/LineItemGroup/9295724684) with its components nested under components. Components are not repeated at the top level, and the offer’s shopify_line_item_ids lists the component line items.
Timeline
Section titled “Timeline”GET /preorders/orders/:id/timeline returns the dashboard order page’s activity log, newest first. It works for orders Shopify hasn’t synced yet and for back-in-stock orders. shown_on_dashboard: false marks events the dashboard doesn’t list.
{ "id": "…", "shopify_order_id": "gid://shopify/Order/6123456789012", "name": "#1042", "events": [ { "event_type": "payment_collection_failed", "description": "Payment collection of 315.0 USD failed: Card declined", "triggered_by": "automatic", "occurred_at": "2026-10-19T12:00:00.000Z", "shown_on_dashboard": true } ]}Per-order actions
Section titled “Per-order actions”Successful actions return 202 with the full order plus an outcome object that reports what happened. Failures return 422 {errors: [...]}; an action the order’s state does not allow returns 409.
| Method | Path | Dashboard action |
|---|---|---|
POST |
/preorders/orders/:id/release |
Release |
POST |
/preorders/orders/:id/open_scheduled_fulfillments |
Open |
POST |
/preorders/orders/:id/hold_fulfillments |
Hold fulfillments |
POST |
/preorders/orders/:id/split_fulfillment_orders |
Split fulfillment orders |
POST |
/preorders/orders/:id/reschedule_fulfillment |
Reschedule fulfillment |
POST |
/preorders/orders/:id/reschedule_payment |
Reschedule payment |
POST |
/preorders/orders/:id/collect_payment |
Collect payment |
POST |
/preorders/orders/:id/send_confirmation |
Resend confirmation email |
POST |
/preorders/orders/:id/send_sms_confirmation |
Resend confirmation SMS |
POST |
/preorders/orders/:id/send_shipping_update |
Send shipping update |
Release
Section titled “Release”Releases the “On hold” fulfillments, like the dashboard’s Release. Only this app’s holds are released; another app’s holds stay. No order tags are applied and payment state is not checked. No body fields.
outcome.fulfillment_orders lists each fulfillment order Shopify released or rejected (fulfillment_order_id, status, error), with succeeded / failed counts. open_scheduled_fulfillments, reschedule_fulfillment, hold_fulfillments and split_fulfillment_orders return the same outcome shape.
Hold fulfillments
Section titled “Hold fulfillments”Puts every open fulfillment order on the order on hold, preorder items or not, like the dashboard’s Hold fulfillments and the Shopify Flow “Hold all fulfillments” action.
| Body field | Type | Default | Notes |
|---|---|---|---|
reason_notes |
string | default hold note | Shown on the hold in Shopify. |
Fulfillment orders Shopify can’t hold are reported with status: "skipped". Returns 409 on a cancelled or fulfilled order, and 422 when nothing could be held.
Split fulfillment orders
Section titled “Split fulfillment orders”Moves the preorder items into their own fulfillment orders, like the dashboard’s Split fulfillment orders and the Shopify Flow action. Only items whose offer has split fulfillments switched on are moved; an empty outcome.fulfillment_orders means nothing needed splitting. Returns 409 on a cancelled or fulfilled order, and 422 when the order has no preorder offer or Shopify rejects the split. No body fields.
Reschedule fulfillment
Section titled “Reschedule fulfillment”| Body field | Type | Default | Notes |
|---|---|---|---|
fulfill_at |
string | required | ISO 8601 |
send_shipping_update |
boolean | false | Email the customer a shipping update, like the modal’s “Send customers a shipping update”. Sent only when a scheduled fulfillment moved. |
subject, header, description |
string | template text | The shipping update’s text, sent as given. subject and header default to the template’s. Leave description out to send what the modal prefills: “Your preorder is now scheduled to ship on {date}.” (the fulfill_at UTC date, formatted for the shop’s app language) followed by the template text. If you send a description, include the date yourself if you want it. |
outcome.notified is true when the shipping update went out because a scheduled fulfillment moved, as in the dashboard’s banner. A cancelled or refunded order is never emailed.
Collect payment
Section titled “Collect payment”Charges the outstanding balance on the saved payment method, like the dashboard’s Collect payment.
| Body field | Type | Default | Notes |
|---|---|---|---|
send_notifications |
boolean | true | Set false to skip the customer email when the charge fails. |
amount |
string | full balance | e.g. "25.00". Collects part of the balance. Shopify Plus only. |
line_item_ids |
array of strings | none | Collects part of the balance for these products. Each id is a line item’s id, its shopify_line_item_id gid, or the numeric Shopify id; a bundle entry stands for its components. Shopify Plus only. |
With neither amount nor line_item_ids, the full outstanding balance is charged, on any plan. Otherwise:
amountalone charges that amount.line_item_idsalone charges the sum of those lines’line_total(price times quantity, plus tax).- Both:
amountis charged and the lines are recorded with the payment.
Collecting part of the balance off Shopify Plus, an amount over the outstanding balance, or a line that is not on this order returns 422 with a message saying which. If Shopify rejects the charge, its message is returned.
The charge is asynchronous: outcome.payment_reference_id identifies it, and the payments section shows the result once Shopify confirms it. outcome.amount is what was charged and outcome.shopify_line_item_ids the lines it was for. Each entry of payments.payment_attempts, and payments.last_payment_attempt, has the same shopify_line_item_ids (empty for a charge not made by product). Returns 409 on a cancelled order.
Reschedule payment
Section titled “Reschedule payment”| Body field | Type | Default | Notes |
|---|---|---|---|
due_at |
string | required | Calendar date in YYYY-MM-DD. Stored as noon UTC on that day. |
outcome has old_due_at and new_due_at. Schedule change only; no money moves.
Notifications
Section titled “Notifications”send_confirmation: resends the confirmation email (outcome.sent,event_id).send_sms_confirmation: resends the confirmation SMS, or says why it can’t be sent.send_shipping_update: emails a shipping update.subject,headeranddescriptionare optional. Refused for a cancelled order.
Bulk actions
Section titled “Bulk actions”POST /preorders/orders/bulk_<action>{ "order_ids": ["5f36c941-…", "gid://shopify/Order/6123456789012", 6123456789013], ... }order_ids are our ids or Shopify order ids / gids, at most 1000, all of which must be preorder orders in this shop (otherwise 422). They run on the dashboard’s bulk workers.
To act on every order matching a filter, like the dashboard’s “Select all” across pages, send "apply_to_all": true instead of order_ids, with any of the list filters (no filters means every preorder order):
POST /preorders/orders/bulk_release{ "apply_to_all": true, "financial_status": "partially_paid", "offer_id": "5f36c941-…" }There is no 1000 limit. order_count is how many orders matched when you submitted; the worker resolves the filters again when it runs. Sending order_ids with apply_to_all, a filter without apply_to_all, a bad filter value, or filters that match no order returns 422.
| Method | Path | Body fields |
|---|---|---|
POST |
/preorders/orders/bulk_release |
none |
POST |
/preorders/orders/bulk_open_scheduled_fulfillments |
none |
POST |
/preorders/orders/bulk_hold_fulfillments |
reason_notes (optional) |
POST |
/preorders/orders/bulk_split_fulfillment_orders |
none |
POST |
/preorders/orders/bulk_reschedule_fulfillment |
exactly one of fulfill_at or anchor_days; send_shipping_update, subject, header, description (optional, as on the single action) |
POST |
/preorders/orders/bulk_reschedule_payment |
due_at (YYYY-MM-DD) |
POST |
/preorders/orders/bulk_collect_payment |
send_notifications (optional, default true). Charges each order’s full balance; paid and cancelled orders are reported failed |
POST |
/preorders/orders/bulk_send_confirmation |
none |
POST |
/preorders/orders/bulk_send_shipping_update |
subject, header, description (optional) |
The response is 202:
{ "job_id": "uuid", "operation": "release", "status": "queued", "status_url": "/api/v2/external/preorders/orders/jobs/<job_id>", "order_count": 3 }Polling
Section titled “Polling”GET /preorders/orders/jobs/:job_idReturns status (queued, running, completed, failed, cancelled), total, processed, succeeded_count, failed_count, and the per-order succeeded (id, shopify_order_id, name) and failed (plus error) lists. Results are kept for 24 hours. Unlike the dashboard, API bulk runs don’t email a results CSV. See Bulk & Async Jobs.
Thanks for letting us know.
Still stuck? Ask Quinn or chat with support