Preorder API for developers
Manage preorder offers, products, and shipping updates from your own code.
Last updated
The STOQ preorder API lets developers build custom integrations: create and update offers, attach or remove variants, enable and disable offers, manage per-variant settings, and send bulk shipping-update emails, all without touching the STOQ dashboard. Actions you take through the API show up for customers right away, for example enabling an offer makes the preorder button appear on your storefront.
Use this article if you’re integrating STOQ preorders with your own tools, scripts, or a Shopify Flow setup that goes beyond the built-in flow actions.
Before you begin
Section titled “Before you begin”| Requirement | Details |
|---|---|
| API key | Generated automatically per shop. Find it in the Integrations sub-tab (Preorders in the left menu → Settings tab → Integrations); see Set it up below. |
| Content type | All requests and responses are JSON. Include Content-Type: application/json on write requests. |
| Shopify IDs | v2 accepts a Shopify variant ID as a number or a gid (gid://shopify/ProductVariant/123), and always returns Shopify IDs as gids in fields named shopify_<thing>_id. v1 uses the numeric Shopify variant ID. |
Choose your API version
Section titled “Choose your API version”STOQ exposes two external API versions. All paths in this article are relative to the base path for the version you use.
- v2 (
/api/v2/external): the current version. Use it for preorder offer management, creating, configuring, and managing the lifecycle of offers, attaching variants, and configuring widget and shipping settings. Recommended for all new integrations. See the API v2 guide and API v2 reference. - v1 (
/api/v1/external): use it to maintain an existing integration built on the older selling-plan model for preorder offers. See the API v1 reference.
Every endpoint below is listed in those references with full parameters, response schemas, and a Test it button you can call with your own key. Start at the developer docs home to pick an integration path.
What customers will see
Section titled “What customers will see”Enabling or disabling an offer through the API changes whether the preorder button appears on your storefront, but it doesn’t send customers any emails or SMS. Customer notifications, like shipping updates, are sent separately: from the order management endpoints, Shopify Flow, or the send_shipping_update action, which emails customers directly with the subject, header, and description you provide.
Set it up
Section titled “Set it up”On a phone, this menu is collapsed. In the Shopify mobile app, tap the STOQ logo at the top of the screen to open it, then choose the section named below. There is no hamburger icon. See Finding the STOQ menu.
-
Open Integrations
- In the left menu bar of the STOQ app, click Preorders.
- Click the Settings tab.
- Open the Integrations sub-tab.
-
Locate the API section
Scroll to the API section. Your API key is displayed in the Access key field (shown as a password field).
-
Copy the key
Click Copy to copy the key to your clipboard. Keep this key secure: it grants full access to your shop’s STOQ data. See API Key in the developer docs.
-
Reset the key if needed
If you suspect the key has been exposed, click Reset. Existing integrations using the old key will stop working immediately; update them with the new key.
Authentication
Section titled “Authentication”Pass your API key in the X-Auth-Token request header on every call:
X-Auth-Token: <your-api-key>The MCP endpoint (see below) also accepts Authorization: Bearer <your-api-key> for clients that only support standard Authorization headers.
A missing or invalid key returns 401 Unauthorized.
v2 API: preorder offers
Section titled “v2 API: preorder offers”The v2 API is action-driven. Each action declares a path, HTTP verb, human-readable description, and a set of natural-language aliases. You can discover the full live manifest at any time:
GET /api/v2/external/helpScope the manifest to preorder actions only:
GET /api/v2/external/help?prefix=/preorders/offersYou can also fetch a machine-readable skill document (no auth required) that AI agents can ingest:
GET /api/v2/external/preorders/skill.mdOffer lifecycle
Section titled “Offer lifecycle”| Method | Path | What it does |
|---|---|---|
GET |
/preorders/offers |
List all offers (paginated, up to 200 per page). |
GET |
/preorders/offers/:id |
Full offer representation including all capability blocks. |
POST |
/preorders/offers |
Create a new offer. Pass name and optionally internal_name. |
POST |
/preorders/offers/:id/enable |
Activate the offer. On a scheduled offer this starts it now, as the dashboard does: the start date is cleared and an end date is kept. Pass update_inventory_policy: true to also flip attached variants to CONTINUE in Shopify. |
POST |
/preorders/offers/:id/disable |
Pause the offer. On a scheduled or dated offer the schedule is cleared, as the dashboard does, so the offer stays off instead of starting on its start date. Pass update_inventory_policy: true to flip variants back to DENY. |
POST |
/preorders/offers/bulk_enable, /preorders/offers/bulk_disable |
Turn many offers on or off at once, like the bulk actions on the Offers list. Pass the offer ids, and optionally update_inventory_policy: true. |
POST |
/preorders/offers/:id/discard |
Soft-delete the offer. Recoverable via restore. |
POST |
/preorders/offers/:id/restore |
Restore a discarded offer to paused state. |
POST |
/preorders/offers/:id/duplicate |
Copy an offer with all its settings, like the dashboard’s Duplicate. The copy starts off, with no products. |
POST |
/preorders/offers/:id/schedule |
Set a launch window. Pass start and/or end as ISO 8601 timestamps. Scheduling does not enable the offer automatically. |
Lifecycle state machine. The enable, disable, discard, restore, and schedule actions validate the transition. An invalid transition returns 409 Conflict with a message naming the current state and what would unblock it.
Capabilities
Section titled “Capabilities”Each capability has a GET to read its current state and a PATCH for catch-all updates (deep partial, only the fields you send are written). Some capabilities also expose named POST actions for specific, high-frequency operations.
| Capability | GET | PATCH | Named POST actions |
|---|---|---|---|
| Widget | /preorders/offers/:id/widget |
/preorders/offers/:id/widget |
set_button_text, set_badge_text, set_badge_colors, set_button_colors, set_disclaimer_text, set_disclaimer_style, set_billing_widget_text |
| Shipping | /preorders/offers/:id/shipping |
/preorders/offers/:id/shipping |
set_delivery_asap, set_delivery_date, set_delivery_window, set_shipping_text, set_split_order_tag, set_split_transaction_gateway |
| Payments | /preorders/offers/:id/payments |
/preorders/offers/:id/payments |
set_mode, set_deposit_percent, set_discount, set_discount_text, set_option_copy, enable_option, disable_option |
| Limits | /preorders/offers/:id/limits |
/preorders/offers/:id/limits |
set_min_per_order, set_max_per_order, set_max_per_customer, set_total_max |
| Checkout | /preorders/offers/:id/checkout |
/preorders/offers/:id/checkout |
set_terms_text, set_mixed_cart_error_message, set_order_tags, add_order_tag, remove_order_tag |
| Inventory | /preorders/offers/:id/inventory |
/preorders/offers/:id/inventory |
set_provider, set_reservation_timing |
| Markets | /preorders/offers/:id/markets |
/preorders/offers/:id/markets |
add_market, remove_market, set_markets |
| Countdown | /preorders/offers/:id/countdown |
/preorders/offers/:id/countdown |
set_mode, set_style, set_custom_end_date, set_starts_text, set_ends_text, set_unit_labels |
| Integrations | /preorders/offers/:id/integrations |
/preorders/offers/:id/integrations |
(boolean toggles only, use PATCH) |
| Translations | /preorders/offers/:id/translations |
n/a | set, set_many, unset, clear_locale, reset_to_defaults |
| Advanced | /preorders/offers/:id/advanced |
/preorders/offers/:id/advanced |
set_button_text_override, set_custom_css, set_custom_line_item_property, attach_to_shopify_selling_plan, detach_from_shopify_selling_plan |
| Products | /preorders/offers/:id/products |
n/a | add_variants, remove_variants, bulk_toggle_inventory_policy, advance_batches |
Products and variant management
Section titled “Products and variant management”| Method | Path | What it does |
|---|---|---|
GET |
/preorders/offers/:id/products |
Read the product type rule and a variant count summary. |
POST |
/preorders/offers/:id/products/add_variants |
Attach variants to a custom-source offer: shopify_variant_ids, shopify_product_ids (every variant of those products) or shopify_collection_id, all Shopify gids. On other offers, a call with no ids re-runs the selection rule. Runs the Products tab’s pre-flight check first: 409 with code: "preflight_issues" and nothing added when there are issues; resolve runs the dashboard’s fix per issue, skip_preflight: true adds anyway. Returns a pollable job with its own job_id. |
POST |
/preorders/offers/:id/products/remove_variants |
Remove variants from a custom-source offer: shopify_variant_ids (Shopify variant gids). |
POST |
/preorders/offers/:id/products/advance_batches |
Batch offers only: move variants to their next batch. |
POST |
/preorders/offers/:id/products/bulk_toggle_inventory_policy |
Set attached variants’ Shopify inventory policy to CONTINUE or DENY. Always returns a pollable job. Omit shopify_variant_ids to apply to all attached variants. |
GET |
/preorders/offers/:id/products/variants |
The offer’s Products table, paginated, with search, sort, product_status and limit_reached; group_by=product lists products with their variants. include_inventory=true adds live inventory. |
GET |
/preorders/offers/:id/products/variants/:shopify_variant_id |
Per-variant settings for one attached variant. |
PATCH |
/preorders/offers/:id/products/variants/:shopify_variant_id |
Update per-variant overrides: shipping_text and max_count. Send null to clear an override and fall back to the offer-level default. On an offer with batches on, max_count is refused (422): set limits per batch with batch_limits. |
POST |
/preorders/offers/:id/products/variants/:shopify_variant_id/toggle_inventory_policy |
Set one variant’s inventory policy now. |
POST |
/preorders/offers/:id/products/variants/:shopify_variant_id/reset_count |
Reset one variant’s preorder count to 0 on this offer. |
GET |
/preorders/products/tags |
The shop’s product tags (first 250, alphabetically), as the tag picker lists them. |
GET |
/preorders/products/tags/count |
How many products carry any of tags, as the tag picker counts them. |
Orders
Section titled “Orders”Read preorder orders and run the order actions the STOQ dashboard has. Each action affects only that one order. There is no API to cancel, refund or edit tags on an order.
| Method | Path | What it does |
|---|---|---|
GET |
/preorders/orders |
List preorder orders (paginated). |
GET |
/preorders/orders/:id |
Full order representation: state, payments, fulfillment, notifications, tags, offers, line items. |
GET |
/preorders/orders/:id/payments, /fulfillment, /notifications |
One block of the full read. |
GET |
/preorders/orders/:id/timeline |
The order’s activity log, newest first. |
POST |
/preorders/orders/:id/release |
Release the order’s “On hold” fulfillments. Every hold on those fulfillments is released, as in Shopify admin. No tags are applied. |
POST |
/preorders/orders/:id/open_scheduled_fulfillments |
Open scheduled fulfillments now. |
POST |
/preorders/orders/:id/hold_fulfillments |
Put all of the order’s fulfillments on hold. Pass reason_notes to set the note Shopify shows on the hold. |
POST |
/preorders/orders/:id/split_fulfillment_orders |
Move the preorder items into their own fulfillment orders, for offers with split fulfillments on. |
POST |
/preorders/orders/:id/reschedule_fulfillment |
Move the fulfillment date. Pass fulfill_at (ISO 8601). Pass send_shipping_update: true to also email the customer a shipping update, like Send customers a shipping update in the dashboard. |
POST |
/preorders/orders/:id/collect_payment |
Charge the remaining balance now using the customer’s saved payment method. Same as Collect payment on the order page. Pass send_notifications: false to skip the customer email if the charge fails. |
POST |
/preorders/orders/:id/reschedule_payment |
Move the remaining-balance due date. Pass due_at as YYYY-MM-DD. Changes the schedule only; no money moves. |
POST |
/preorders/orders/:id/send_confirmation, /send_sms_confirmation, /send_shipping_update |
Resend the confirmation email or SMS, or send a shipping update. |
In the reads, :id can be STOQ’s order id, the Shopify order ID or gid, the order name or number, or the customer’s email. The actions take only STOQ’s order id or the Shopify order ID or gid. Shopify ids in responses are gids named shopify_<thing>_id, e.g. shopify_order_id. See the Orders reference for the response shape and the bulk endpoints.
MCP server
Section titled “MCP server”If you prefer tool calling over raw HTTP, the v2 API is also available as an MCP server. Instead of one tool per action, it has a search tool to find actions and execute_read and execute_write tools to run them. Only the /preorders, /back_in_stock, and /account actions can be run this way:
POST /api/v2/external/mcpAuthenticate with X-Auth-Token: <key> or Authorization: Bearer <key>. See the MCP server docs.
v1 API: preorder offers (selling plans)
Section titled “v1 API: preorder offers (selling plans)”The v1 external API also exposes a selling-plan-level CRUD surface for preorder offers. Use the v2 API for new integrations; this surface remains available for existing integrations.
| Method | Path | What it does |
|---|---|---|
GET |
/api/v1/external/preorders |
List enabled offers. Filter by name. Paginated. |
GET |
/api/v1/external/preorders/:id |
Single offer. Includes shopify_variant_ids. |
POST |
/api/v1/external/preorders |
Create an offer. product_variants_source is restricted to "custom" via the API. |
PUT/PATCH |
/api/v1/external/preorders/:id |
Update offer settings. product_variants_source is restricted to "custom" via the API. Changing enabled turns the offer on or off the way the dashboard does, so a scheduled offer’s dates are updated too. |
DELETE |
/api/v1/external/preorders/:id |
Disable and soft-delete the offer. |
POST |
/api/v1/external/preorders/:id/add_variant |
Add variants by Shopify variant ID array. |
DELETE |
/api/v1/external/preorders/:id/remove_variant |
Remove variants. Optionally pass inventory_policy (CONTINUE or DENY) to update Shopify inventory policy on removal. |
PATCH |
/api/v1/external/preorders/:id/update_variant_settings |
Set shipping_text and/or preorder_max_count for one variant on this offer. Send null (or an empty string) to clear an override. |
POST |
/api/v1/external/preorders/:id/bulk_toggle_inventory_policy |
Bulk-update Shopify inventory policy for a list of variants. Async (batches of 1,000 variant IDs). |
GET |
/api/v1/external/preorders/:id/product_variants |
Variant list for this offer. Filter by shopify_product_id or shopify_variant_id. |
GET |
/api/v1/external/preorders/:id/variant_count |
Count of variants attached to this offer. |
GET |
/api/v1/external/preorders/:id/job_status |
Status of any running background job for this offer (e.g. bulk inventory policy update). |
DELETE |
/api/v1/external/preorders/:id/cancel_job |
Cancel a running background job. |
GET |
/api/v1/external/preorders/jobs |
All active background jobs for this shop. |
POST |
/api/v1/external/preorders/send_shipping_update |
Send a custom shipping update email to a list of preorder orders. Requires order_ids or shopify_order_ids, plus subject, header, and description. Async. |
Rate limits
Section titled “Rate limits”Both v1 and v2 use a cost-weighted token bucket, scoped per API key. Full details in Rate Limits.
| Request type | Cost |
|---|---|
| GET (read) | 1 point |
| POST / PATCH / PUT / DELETE (write) | 2 points |
| Bucket | 360 points per 60 seconds |
Every response includes rate-limit headers:
X-RateLimit-Limit: 360X-RateLimit-Remaining: 358X-RateLimit-Reset: 1751500860If you exceed the limit you receive 429 Too Many Requests with a Retry-After header indicating how many seconds to wait.
Check your setup
Section titled “Check your setup”Call GET /api/v2/external/help with your X-Auth-Token header. A 200 response with the action manifest confirms your key and header are set up correctly; a 401 means the header name, key, or format is wrong.
Troubleshooting
Section titled “Troubleshooting”You get 401 Unauthorized
Check that you’re sending X-Auth-Token (or Authorization: Bearer for the MCP endpoint) with a valid key. Reset and re-copy the key from Integrations if you’re not sure it’s current.
You get 404 Not Found
The offer, variant, or resource doesn’t exist for this shop, or the v2 action path isn’t registered. Check GET /api/v2/external/help for the current list of valid paths.
You get 409 Conflict on enable, disable, discard, restore, or schedule
The requested state transition isn’t valid from the offer’s current state. Read the errors array, it names the current state and what would unblock the transition.
You get 422 Unprocessable Entity
A validation failed. Read the errors array for the specific reason (v2 returns an array; v1 returns a single error string).
You get 429 Too Many Requests
You’ve exceeded the rate limit. Wait the number of seconds in the Retry-After header before retrying.
You called bulk_toggle_inventory_policy but Shopify's inventory policy hasn't changed yet
The update runs asynchronously and can take several minutes on large variant lists. Poll GET .../job_status (v1) or the jobs endpoint to track progress.
Limitations
Section titled “Limitations”Storefront enforcement of the per_customer.max and total.max limit fields may not be fully active. Test in a non-production environment before relying on them as hard caps.
Should I use v1 or v2 for a new preorder integration?
Use the v2 API for new integrations. It exposes:
- A richer action surface with natural-language aliases.
- A self-describing
/helpmanifest. - An MCP server endpoint.
- Per-capability read/write endpoints that mirror the dashboard’s structure.
The v1 selling-plan endpoints remain available for existing integrations.
Why does my v1 create/update call fail with 'product_variants_source is restricted to custom'?
The v1 API enforces product_variants_source: "custom" only. You cannot create collection-based or all-products offers via the API, use the STOQ dashboard for those offer types. Once the offer exists in the dashboard you can manage its custom-variant list (add/remove) via the API.
Does enabling or disabling an offer via the API notify customers?
No. Enabling or disabling an offer changes whether the preorder button is visible on your storefront; it does not send any notification emails or SMS to customers who have already placed preorders. Customer notifications (confirmation, shipping update) are triggered separately through the order management endpoints or Shopify Flow.
What happens to existing preorder orders when I discard an offer via the API?
Discarding an offer soft-deletes it (sets discarded_at).
- STOQ removes the Shopify selling plan group, detaches variants, and resets variant metafields.
- Existing customer orders already placed against this offer are not affected: they remain in Shopify and STOQ continues to track them.
- You can restore the offer via
POST .../restore.
Can I use the API to send a shipping update email to all customers on a preorder?
Yes, via the v1 endpoint POST /api/v1/external/preorders/send_shipping_update.
- Pass an array of STOQ order IDs (
order_ids) or Shopify order IDs (shopify_order_ids), along with asubject,header, anddescription. - The emails are sent asynchronously via a background job.
- You cannot pass both
order_idsandshopify_order_idsin the same call.
See also the preorder shipping update email article for how these emails are templated.
Can I split one offer's products into separate offers through the API?
There’s no single “convert” endpoint. Create each new offer first (API-created offers use the custom product source), then add variants to it with the offer’s add_variants action. A variant can be on more than one offer, so to also take it off the grouped offer, either pass remove_from_conflicting_offers: true on add_variants or call the grouped offer’s remove_variants. For a large migration, STOQ support can also do it for you.
Related guides
Section titled “Related guides”Thanks for letting us know.
Still stuck? Ask Quinn or chat with support