List Release Candidates
GET/api/sales-orders/release-candidates
Lists open sales orders that were never dispatched, as one flat list. Every stranded order appears exactly once, releasable and blocked alike, and each one carries its own diagnosis, so releasable_count plus blocked_count always equals the number of entries in orders.
orders:readGrant this scope to your token under Settings → Developer → Personal Access Tokens.
An order is a candidate only when all of the following hold: it is open and not fully fulfilled; it holds no fulfillment order that has already been dispatched (a cancelled one, or one still unsubmitted or rejected, does not count as dispatched); it came from a sales channel rather than being entered by hand; it is at least minimum_age_days business days old; and it is not sitting in a state that was deliberately configured. The age floor counts business days (Mon-Fri), so a weekend does not age an order towards it. It exists because dispatch runs from a queue, so a just-placed order is legitimately undispatched for a short while.
Deliberately configured states are excluded outright rather than reported as blockers: an order under an unexpired hold, an order whose warehouse has auto-dispatch switched off or is set to manual fulfillment, and an order before the configured inventory start date. Those are the system doing exactly what it was told to do, so they are not stranded and never appear in this response.
Automatic dispatch runs once per order, when the order is created or edited. If that single attempt does not go through — or if it ran while something was still blocking the order, such as an unmapped line or a warehouse with no address — nothing tries again and the order stays open indefinitely. Clearing the blocker afterwards does not re-trigger dispatch either. This endpoint finds those orders so they can be reviewed and released.
Authentication: Requires Bearer token.
Query parameters: none.
Response fields:
- releasable_count (integer): How many entries in orders have nothing blocking them and can be dispatched now.
- blocked_count (integer): How many entries in orders are still blocked.
- scanned_count (integer): How many candidate orders were diagnosed in this pass. At most 500 are diagnosed per request. It can exceed the number of entries in orders by the handful of orders that turned out to have nothing left to dispatch — those finished by another route and are omitted.
- candidate_count (integer): Total number of candidate orders, before the 500-order scan limit is applied.
- oldest_days_open (integer): Age of the longest-waiting candidate order, counted in whole calendar days in the account's configured timezone rather than elapsed 24-hour periods. An order placed late yesterday evening local time therefore reports 1, even though fewer than 24 hours have passed. Measured across every candidate, not just the scanned window, so it stays accurate when truncated is true. 0 when there are no candidates.
- minimum_age_days (integer): The configured age floor, in whole business days (Mon-Fri; weekends do not age an order towards it). Orders younger than this are excluded from every count in this response. 0 means no floor is applied.
- truncated (boolean): True when candidate_count is greater than scanned_count, i.e. more candidates exist than were diagnosed.
- orders (array of objects): One entry per stranded order, releasable ones first and then blocked ones, each group longest-waiting first. Each entry has id (integer), sales_order_number (string, nullable), customer_name (string, nullable), sales_channel_name (string, nullable), order_date (string, Y-m-d, nullable), days_open (integer, whole calendar days in the account's configured timezone, the same count as oldest_days_open; minimum_age_days is measured in business days instead, and an order's business-day age is never greater than its calendar-day age, so a returned entry is still never younger than the floor), and diagnosis (object, see below).
Diagnosis object (returned on every entry in orders):
- sales_order_id (integer)
- blocker (string, nullable): Blocker code. Null when nothing is blocking the order.
- blocker_label (string, nullable): Short human-readable name for the blocker.
- detail (string, nullable): One sentence explaining the reason for this specific order.
- is_releasable (boolean)
- is_operator_actionable (boolean): False when the blocker only clears with time or incoming stock (awaiting_stock), and false whenever blocker is null; true otherwise.
Blocker codes that can be reported here: unmapped_lines, no_shipping_method, shipping_method_excluded, awaiting_stock, out_of_sync, no_warehouse_on_lines, warehouse_missing_address, shipping_address_invalid.
When an order is blocked for more than one reason, the reported blocker is the one worth acting on. The remaining codes in the wider blocker set — not_from_sales_channel, held, before_inventory_start_date, warehouse_auto_dispatch_disabled, warehouse_manual_fulfillment — describe the deliberately configured states above, so an order carrying only those is never returned by this endpoint.
Request
Responses
- 200
- 401
- 403
- 429
OK
Response Headers
Unauthenticated — the bearer token is missing, revoked, expired, or malformed. Never retry automatically; fix the credential. See the Errors guide.
Forbidden — the token lacks a required scope, the endpoint is not available to API tokens, or the user behind the token lacks the permission. A human must adjust the token scopes or user permissions; do not retry.
Rate limited — platform limit is 1,000 requests/min; individual tokens may carry lower limits. Honor the Retry-After header before retrying. See the Rate Limits guide.