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
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
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:
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
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 toGET /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 /ordersfor your operational order queue - use
POST /orderswhen your backend wants to create a pending order from catalog line items - use
POST /orders/:id/fulfillfor a server-side fulfillment trigger on a pending order - use
POST /orders/:id/refundfor merchant-driven refunds without the dashboard - use
POST /orders/:id/shipandPOST /orders/:id/deliverto ship physical items and mark them delivered (see Physical products) - use
GET /disputesfor chargebacks and payment-risk review
Invoices & payments
- use
GET /invoiceswhen you need the lower-level invoice resource directly - use
POST /payment-linksorPOST /payment-links/:id/togglefor sales-link workflows - use
GET /coupons/code/:codefor server-side coupon validation by code
Subscriptions & customers
- use
POST /subscriptions/:id/pauseorPATCH /subscriptions/:id/custom-fieldsfor recurring billing workflows - use
POST /customers/:id/wallet/creditorPOST /customers/:id/wallet/debitfor store-credit adjustments - use
GET /customers/:id?include_affiliate=truewhen you need customer detail plus affiliate summary in one response
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=truewhen you need product reads with inline variant prices - use
GET /shipping-zones,POST /shipping-zones,PUT /shipping-zones/:id, orDELETE /shipping-zones/:idto manage the flat shipping rates of physical products (products.*scopes, see Shipping zones)
Analytics & affiliates
- use
GET /analytics/revenuefor revenue totals and a daily timeline in the shop’s time zone - use
GET /affiliates/customersorPOST /affiliates/applications/:id/approvefor 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, orPUT /store/layoutfor storefront automation - use
GET /themes/:id/control/document/draft,PUT /themes/:id/control/document, andPOST /themes/:id/control/publishfor revision-gated ThemeDocument automation - use
GET/POST /themes/:id/control/settingsfor settings-derived themes andPOST /themes/:id/control/previewfor previews
Licenses & webhooks
- use
GET /licenses,POST /licenses,PATCH /licenses/:id, orDELETE /licenses/:idfor license support workflows - use
POST /webhooks/logs/:id/retryorPOST /webhooks/:id/rotate-secretfor webhook ops workflows
Security
- use
GET /security/audit-trailorGET /security/session-trail/:session_idfor security and support workflows
- the public storefront flow already had
POST /v1/storefront/coupons/check GET /coupons/code/:codeis the server-to-server Dev API equivalent
Theme automation shortcut
If you came here for themes, the usual endpoint family is:GET /themesPOST /themes/control/createGET /themes/{id}/control/settingsPOST /themes/{id}/control/settingsGET /themes/{id}/control/documentGET /themes/{id}/control/document/draftPUT /themes/{id}/control/documentPOST /themes/{id}/control/document/validateGET /themes/{id}/control/document/schemaPOST /themes/{id}/control/previewDELETE /themes/{id}/control/preview/{sessionId}POST /themes/{id}/control/publish
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:
- Editing with AI
- Visual themes
- this API reference for the exact endpoint details
Response format
All responses return JSON with a consistent structure:- Success Response
- List Response
- Error Response
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
UseX-Request-Id to correlate your logs with Shoppex responses. Shoppex returns the same header on both success and error responses.
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 a429 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-KeyX-Idempotency-Key
Idempotency-Status header:
createdwhen the request created a new paymentcachedwhen Shoppex replayed the stored responseprocessingwhen the same key is still in flightbypassedwhen idempotency was intentionally skipped
- 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
5xxresponses 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
- 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.- Cursor-based
- Page-based
Use cursor-based pagination for feed-style lists like products, invoices, orders, customers, and subscriptions.Response:
Parameters
Basic usage
First request
Next page
Use thenext_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.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:GET /customersGET /ordersGET /invoices
filters uses field:value pairs separated by commas.
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.