Skip to content

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/:id and its section URLs) take any order reference; see Looking up an order.
  • Actions (POST /preorders/orders/:id/...) and bulk order_ids take only our id or a Shopify order id / gid, so they never act on a guess. Anything else returns 404 (single) or 422 (bulk).

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.

:id in the reads can be:

  • STOQ’s order id (id in 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) or 1234
  • 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.

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 }]. name is the line’s title and variant title, as the row shows it.
  • linked_offers: [{ offer_id, offer_internal_name, deleted }]. deleted is true for an offer that has since been deleted, which the row marks.

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). null when the order has no alert linked.
  • notification: the Type and Notification sent at columns: { channel, sent_at }. channel is email or sms, or push for an old web push send (STOQ no longer sends web push). null when 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
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.

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 }
]
}

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

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.

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.

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.

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.

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:

  • amount alone charges that amount.
  • line_item_ids alone charges the sum of those lines’ line_total (price times quantity, plus tax).
  • Both: amount is 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.

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.

  • 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, header and description are optional. Refused for a cancelled order.
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 }
GET /preorders/orders/jobs/:job_id

Open in the API reference

Returns 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.

Was this page helpful?

Still stuck? Ask Quinn or chat with support