---
title: "Preorder API for developers"
description: "Manage preorder offers, products, and shipping updates from your own code."
url: https://help.stoqapp.com/preorders/preorder-api/
product: "Preorders"
section: "Notifications and integrations / Apps and automation"
last_updated: 2026-10-27
---

# Preorder API for developers

> Manage preorder offers, products, and shipping updates from your own code.

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

| 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

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](https://docs.stoqapp.com/v2/docs/api-v2/) and [API v2 reference](https://docs.stoqapp.com/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](https://docs.stoqapp.com/v1/).

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](https://docs.stoqapp.com/docs/index/) to pick an integration path.

## 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

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](https://help.stoqapp.com/general/getting-started-with-stoq/#finding-the-stoq-menu).

1. **Open Integrations**

   1. In the left menu bar of the STOQ app, click **Preorders**.
   2. Click the **Settings** tab.
   3. Open the **Integrations** sub-tab.

2. **Locate the API section**

   Scroll to the **API** section. Your API key is displayed in the **Access key** field (shown as a password field).

3. **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](https://docs.stoqapp.com/docs/api-key/) in the developer docs.

4. **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

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

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/help
```

Scope the manifest to preorder actions only:

```
GET /api/v2/external/help?prefix=/preorders/offers
```

You can also fetch a machine-readable skill document (no auth required) that AI agents can ingest:

```
GET /api/v2/external/preorders/skill.md
```

### 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

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

| 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

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](https://docs.stoqapp.com/v2/docs/api-v2/orders/) for the response shape and the bulk endpoints.

### 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/mcp
```

Authenticate with `X-Auth-Token: <key>` or `Authorization: Bearer <key>`. See the [MCP server docs](https://docs.stoqapp.com/mcp/).

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

> **Caution: Deprecated: offer-level `shipping_text`**
>
> The offer-level `shipping_text` field on `POST` / `PUT` `/api/v1/external/preorders` is **deprecated**, and no longer rendered on the storefront/checkout for preorder offers. It is still accepted for backwards compatibility, but responses include a `warnings` entry (`code: "deprecated_field"`). Use `preorder_shipping_text` together with `delivery_type` and `delivery_at` / `delivery_after_n_intervals` instead. The per-variant `shipping_text` override on `update_variant_settings` is **not** deprecated.

## Rate limits

Both v1 and v2 use a cost-weighted token bucket, scoped per API key. Full details in [Rate Limits](https://docs.stoqapp.com/docs/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: 360
X-RateLimit-Remaining: 358
X-RateLimit-Reset: 1751500860
```

If you exceed the limit you receive `429 Too Many Requests` with a `Retry-After` header indicating how many seconds to wait.

## 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

### 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

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.

## FAQ

### Should I use v1 or v2 for a new preorder integration?

Use the v2 API for new integrations. It exposes:

1. A richer action surface with natural-language aliases.
2. A self-describing `/help` manifest.
3. An MCP server endpoint.
4. 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`).

1. STOQ removes the Shopify selling plan group, detaches variants, and resets variant metafields.
2. Existing customer orders already placed against this offer are not affected: they remain in Shopify and STOQ continues to track them.
3. 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`.

1. Pass an array of STOQ order IDs (`order_ids`) or Shopify order IDs (`shopify_order_ids`), along with a `subject`, `header`, and `description`.
2. The emails are sent asynchronously via a background job.
3. You cannot pass both `order_ids` and `shopify_order_ids` in the same call.

See also the [preorder shipping update email](https://help.stoqapp.com/preorders/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

- [Preorders with Shopify Flow](https://help.stoqapp.com/preorders/preorders-with-shopify-flow/): Automate preorder actions using Shopify Flow triggers and actions without writing code.
- [Find and read preorder orders](https://help.stoqapp.com/preorders/find-and-read-preorder-orders/): Locate individual preorder orders and read their status in STOQ.
- [Preorder shipping update email](https://help.stoqapp.com/preorders/preorder-shipping-update-email/): Customize and send shipping update emails to preorder customers.
- [Preorder advanced settings](https://help.stoqapp.com/preorders/preorder-advanced-settings/): Configure line item properties, button text overrides, and custom CSS on an offer.
- [Connect Klaviyo for preorders](https://help.stoqapp.com/preorders/connect-klaviyo-for-preorders/): Route preorder events to Klaviyo for advanced email and SMS flows.
