Skip to main content
This page is the Dev API overview, not the best first page for every workflow.Use this quick routing:
  • if you want your first generic Dev API call, start with Quick start
  • if you want to build or customize a theme with AI, start with Editing with AI
  • if you want theme automation endpoints specifically, read Visual themes first and use this API section as reference after that

API retirements

2026-10-17 — Variant fields retired. The nine field-definition and field-option endpoints under /products/:id/variants/fields have been removed. Stop polling these endpoints. Product variants, their option_values, stock, pricing, and serial-delivery endpoints remain available.

Guide vs reference

  • /themes/* explains the ThemeDocument workflow and when to use settings, document, preview, and publish operations
  • /api-reference/* explains the Dev API shape, auth, scopes, and endpoint details
Use the theme pages to understand the workflow. Use the API reference when you already know the workflow and need the exact request shape.

Official SDKs

If you do not want to hand-write requests, use an official SDK for your language.

SDKs & libraries

Official Shoppex SDKs, install commands, public repos, and language examples.

Base URL

All API requests must be made to:

Authentication

The Shoppex API uses Bearer token authentication. You can authenticate with:
  • an API key like shx_..., for server-to-server integrations you manage directly
  • an OAuth2 access token like shpat_..., for installable third-party apps
An internal sync worker uses Authorization: Bearer shx_your_api_key_here. An installable ERP app uses Authorization: Bearer shpat_your_access_token_here. Include the token in the Authorization header:
Keep your API keys secure. Never expose them in client-side code or public repositories.
API keys and OAuth2 access tokens can both be limited with scopes, for example products.read or themes.write. See Authentication for the full scope catalog, how to create and rotate keys, and the OAuth2 authorization flow. Use GET /me/capabilities to inspect an active key’s scopes, enabled payment methods, and (for OAuth2) issuing client at runtime. See Authentication for the full response shape.

Request format

All requests must:
  • Use Content-Type: application/json
  • Send JSON-encoded request bodies
  • Include the Authorization header
Image uploads use JSON too: send an image URL or base64 bytes to POST /dev/v1/media, then pass the returned id in a product’s image_ids. See Product images.

OAuth2 flow

For third-party apps, Shoppex also supports an OAuth2 authorization-code flow. The merchant browser is redirected to GET /dev/v1/oauth/authorize and approves on the Shoppex dashboard consent page. PKCE (S256) is supported. Your backend then exchanges the returned shoa_... code at POST /dev/v1/oauth/token for a shpat_... access token. The token endpoint returns standard OAuth2 JSON, not the normal Shoppex API envelope. Every other /dev/v1/* endpoint you call afterward still uses the standard Shoppex response format. See Authentication for the full flow with request and response examples.

Payment retries and gateway switches

Shoppex can create more than one internal payment attempt for the same invoice. A customer can start with PayPal, close the checkout, reopen the invoice, and finish with Stripe. This catches most people off guard. Treat the Shoppex invoice or payment ID as the source of truth, not a single provider session or order ID.

Orders & disputes

  • use GET /orders for your operational order queue
  • use POST /orders when your backend wants to create a pending order from catalog line items
  • use POST /orders/:id/fulfill for a server-side fulfillment trigger on a pending order
  • use POST /orders/:id/refund for merchant-driven refunds without the dashboard
  • use POST /orders/:id/ship and POST /orders/:id/deliver to ship physical items and mark them delivered (see Physical products)
  • use GET /disputes for chargebacks and payment-risk review

Invoices & payments

  • use GET /invoices when you need the lower-level invoice resource directly
  • use POST /payment-links or POST /payment-links/:id/toggle for sales-link workflows
  • use GET /coupons/code/:code for server-side coupon validation by code

Subscriptions & customers

  • use POST /subscriptions/:id/pause or PATCH /subscriptions/:id/custom-fields for recurring billing workflows
  • use POST /customers/:id/wallet/credit or POST /customers/:id/wallet/debit for store-credit adjustments
  • use GET /customers/:id?include_affiliate=true when you need customer detail plus affiliate summary in one response
API wallet credits count as merchant credit. Outstanding credit without verified payment funding has a USD 10,000 limit per wallet, converted to the wallet currency. The limit applies to the total across requests. A request that exceeds the limit fails without adding credit. Send wallet credits and debits with an Idempotency-Key. An idempotency key belongs to one money movement; use a new key for a new movement. After a 5xx answer or a timeout, send the same request again with the same key within 24 hours. If the first request moved the balance, you get its result, and the balance does not move a second time. For OAuth apps, this also holds after a token refresh. These same-key retries are safe only for requests first sent after the Dashboard 9 release. For a request first sent before that release, a retry gets only an answer that was stored for the same API key or access token. For such a request, a retry after a 5xx answer or a timeout, or for OAuth apps after a token refresh, can move the balance again, so read the wallet balance and compare it before you send it again. Without a key, a 5xx answer or a timeout does not tell you whether the balance moved. See Store credit for refund exceptions and purchase verification.

Products & variants

  • use GET /products?include_variants=true when you need product reads with inline variant prices
  • use GET /shipping-zones, POST /shipping-zones, PUT /shipping-zones/:id, or DELETE /shipping-zones/:id to manage the flat shipping rates of physical products (products.* scopes, see Shipping zones)

Analytics & affiliates

  • use GET /analytics/revenue for revenue totals and a daily timeline in the shop’s time zone
  • use GET /affiliates/customers or POST /affiliates/applications/:id/approve for affiliate program automation
GET /affiliates/customers/:customerId/performance returns products[].total_revenue in USD. The response’s currency is "USD"; shop_currency identifies the shop’s configured currency and does not change the revenue currency. Breaking change: supplier sourcing removed (2026-10-18). Supplier source and supplier order dashboard routes now return 404. Catalog responses no longer include supplier_backed or max_unit_price, and reseller program settings no longer include dynamic_delivery_enabled. Webhook create, update and test calls no longer accept reseller:supplier_order.delivered or reseller:supplier_order.failed. Update storefront consumers and rebuild code storefronts before the backend release. Ordinary wholesale orders and webhook product delivery remain available. Breaking change: analytics reports removed (2026-10-03). GET /dev/v1/analytics/reports, POST /dev/v1/analytics/reports, GET /dev/v1/analytics/reports/:id/download, POST /dev/v1/analytics/reports/:id/generate and DELETE /dev/v1/analytics/reports/:id now return 404. Scheduled reports are no longer generated or emailed, and existing report files can no longer be downloaded. Use GET /dev/v1/analytics/revenue for revenue figures. The MCP tools analytics_reports_list and analytics_report_generate are removed with them.

Workflows

Breaking change — run data scopes (2026-09-29): GET /dev/v1/workflows/runs and GET /dev/v1/workflows/runs/:id require orders.read and customers.read in addition to workflows.read. Run errors and step outputs can contain customer and order data. Keys missing either data scope receive 403.

Telegram

Breaking change — warehouse-restock alias removed (2026-09-28): POST /dev/v1/telegram/bots/:botId/broadcasts/warehouse-restock now returns 404. Use POST /dev/v1/telegram/bots/:botId/broadcasts/restock with the same request body. Idempotency keys are scoped per path, so a retry that moves to the new path is not deduplicated against the old one.

Store & themes

  • use GET /store/branding, POST /store/domains/additional, GET /store/domains/additional/:id/verification, or PUT /store/layout for storefront automation
  • use GET /themes/:id/control/document/draft, PUT /themes/:id/control/document, and POST /themes/:id/control/publish for revision-gated ThemeDocument automation
  • use GET/POST /themes/:id/control/settings for settings-derived themes and POST /themes/:id/control/preview for previews

Licenses & webhooks

  • use GET /licenses, POST /licenses, PATCH /licenses/:id, or DELETE /licenses/:id for license support workflows
  • use POST /webhooks/logs/:id/retry or POST /webhooks/:id/rotate-secret for webhook ops workflows

Security

  • use GET /security/audit-trail or GET /security/session-trail/:session_id for security and support workflows
Coupon note:
  • the public storefront flow already had POST /v1/storefront/coupons/check
  • GET /coupons/code/:code is the server-to-server Dev API equivalent
The reliable pattern: use Shoppex invoice/payment responses, listen to Shoppex webhooks, and make your fulfillment logic idempotent. Do not assume one invoice always maps to exactly one provider-side payment session.

Theme automation shortcut

If you came here for themes, the usual endpoint family is:
  • GET /themes
  • POST /themes/control/create
  • GET /themes/{id}/control/settings
  • POST /themes/{id}/control/settings
  • GET /themes/{id}/control/document
  • GET /themes/{id}/control/document/draft
  • PUT /themes/{id}/control/document
  • POST /themes/{id}/control/document/validate
  • GET /themes/{id}/control/document/schema
  • POST /themes/{id}/control/preview
  • DELETE /themes/{id}/control/preview/{sessionId}
  • POST /themes/{id}/control/publish
These endpoints inspect, revision-save, preview, and publish validated ThemeDocuments and their settings. For document-authoritative themes, read the draft first. Send its revision back as expected_revision when saving, and send the new revision returned by the save as expected_revision when publishing. The MCP tools wrap this exact read → save → publish contract. POST /themes/{id}/control/document/validate checks a document with the save’s rules and saves nothing. It answers valid and issues ({ field, message }). A refused save changes nothing and does not hold its Idempotency-Key, so the corrected retry may reuse it. A 422 has error.subcode invalid_theme_document and lists every issue in error.details. A stale revision answers 409 with error.subcode DRAFT_REVISION_CHANGED and error.current_revision. A settings-derived theme answers 409 with error.subcode settings_derived_theme: change its Builder settings instead. POST /themes/control/create accepts these document-native bases: default, classic, pulse, starlight, clean-minimal, and forkme. Every accepted base maps to a deployed ThemeDocument composition. Retired schemes (apex, nebula, phantom) are no longer valid bases. Existing themes on them keep rendering and stay exportable and importable. The removed shadow and vault schemes no longer render or import. Best reading order:
  1. Editing with AI
  2. Visual themes
  3. this API reference for the exact endpoint details

Response format

All responses return JSON with a consistent structure:

HTTP status codes

All error responses follow the standard error envelope format. See the Error handling guide for the full status-code table and detailed error codes.

Request tracing

Use X-Request-Id to correlate your logs with Shoppex responses. Shoppex returns the same header on both success and error responses.
If you send X-Request-Id: sync-run-42, Shoppex echoes that same id back. If you do not send one, Shoppex generates one for you.

Rate limiting

API requests are limited to protect the service. If you go over the limit, Shoppex returns a 429 Too Many Requests response. Authenticated Dev API requests return X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and (when blocked) Retry-After headers. If you are rate limited, wait until X-RateLimit-Reset or Retry-After before retrying. See the Error handling guide for a header example and the RATE_LIMITED error shape.

Idempotency

All current state-changing /dev/v1 endpoints support idempotent retries. This covers product, category, group, coupon, variant, customer, webhook, ticket, review, blacklist, license, escrow, invoice, and payment writes. Retrying POST /customers after a timeout will not create the same customer twice, and retrying POST /webhooks/:id/test will not spam duplicate test deliveries. Use one of these headers:
  • Idempotency-Key
  • X-Idempotency-Key
Responses include an Idempotency-Status header:
  • created when the request created a new payment
  • cached when Shoppex replayed the stored response
  • processing when the same key is still in flight
  • bypassed when idempotency was intentionally skipped
Important behavior:
  • the replay key is scoped to your authenticated key, the route, and the idempotency key
  • Shoppex also checks the request body for the same method and path
  • the replay window is currently 24 hours
  • 5xx responses are not cached for replay
  • a refusal that changed nothing is not cached either when the endpoint says so (for example the theme document save, and a stale or missing If-Match): the key stays free, so the corrected request may reuse it
In other words:
  • same key + same body -> cached response
  • same key + different body -> validation error
  • same key while the first request is still in flight -> processing
  • key sent but Shoppex cannot check it right now -> 503 SERVICE_UNAVAILABLE, the request did not run; retry with the same key

Pagination

The Dev API uses two pagination models. Cursor-based pagination covers most resource lists, including products, invoices, and orders. Page-based pagination covers operational lists, such as webhook delivery logs, where page navigation is clearer.
Use cursor-based pagination for feed-style lists like products, invoices, orders, customers, and subscriptions.

Parameters

Basic usage

First request

Response:

Next page

Use the next_cursor value from the previous response.

Final page

The response looks like this when no items remain.

Code examples

Fetch all pages

Async generator (streaming)

TypeScript

Filtering with pagination

Filters work alongside pagination.
Always include the same filters and sorts when you follow a cursor. Changing them invalidates the cursor.

Cursor-based endpoints

Pagination best practices

Use reasonable limits

For a UI, 20-25 items is plenty. For a background sync job, raise the limit to 100. Larger limits increase response time.

Do not store cursors

Cursors work for immediate sequential use. They expire after 24 hours. They also become invalid if the underlying data changes significantly.

Handle empty results

An empty data array with has_more: false is valid. No items match your query.

Respect rate limits

When you fetch all pages, add delays between requests to avoid rate limiting.

Filtering

Core list endpoints now support a shared filtering and sorting contract:
Supported list endpoints for this shared contract:
  • GET /customers
  • GET /orders
  • GET /invoices
filters uses field:value pairs separated by commas.
Legacy single-field query params like status or customerEmail still work where already documented, but new integrations use filters and sorts instead.

API changelog

Clarified — Coupon subscription fields on update (R3, 2026-10-26)

PATCH /dev/v1/coupons/{id} takes subscription_duration and subscription_payments as a pair. A PATCH without subscription_duration keeps both stored values, and subscription_payments sent alone is refused with VALIDATION_ERROR. To change how many payments a REPEATING coupon discounts, send subscription_duration: "REPEATING" with the new subscription_payments. The behaviour is unchanged; the endpoint reference now states it.

Changed — Affiliate payout approval (2.0, 2026-10-08)

POST /dev/v1/affiliates/payout-requests/{id}/approve always answers HTTP 409 with CONFLICT: Shoppex no longer pays affiliates for you. To pay a payout request, call POST /dev/v1/affiliates/payout-requests/{id}/force-approve, send the payout to the affiliate from your own wallet, then call POST /dev/v1/affiliates/payout-requests/{id}/complete. The other payout request endpoints are unchanged.

Removed — Replacement Rules (2026-10-03)

All /dev/v1/replacement-rules endpoints, including rule simulation, have been removed. Buyer replacement eligibility and request endpoints have also been removed from the customer API and storefront SDK. Merchant manual replacement remains available through POST /dev/v1/invoices/{uniqid}/replacements. Replacement history remains available through /dev/v1/replacement-events; its retired rule_id, request_id, rule, and request fields and rule filter have been removed.

Disputes pagination and amounts — Dashboard 9 release

GET /dev/v1/disputes now uses keyset cursors tied to the dispute’s creation time and internal ID. Cursors issued by the previous implementation are no longer accepted and return HTTP 422 with VALIDATION_ERROR. Restart pagination without cursor, then follow the returned pagination.next_cursor until has_more is false. Treat cursors as opaque strings; the maximum length is 512 characters. Dispute amount is now the invoice display total in the accompanying currency, correcting the previous USD total paired with a display currency. If the invoice is unavailable, amount is null and amount_unavailable_reason is invoice_unavailable. An existing invoice with no display total returns amount: null and amount_unavailable_reason: display_total_unavailable. When the display total is available, the reason is null. The provider’s disputed amount remains available separately as disputed_amount and disputed_amount_currency.