Wix is a hosted website builder with a built-in eCommerce product (Wix Stores) used by small and mid-size merchants worldwide, heavily in North America, Western Europe and Israel. A Wix merchant runs one site per storefront, and everything a connector needs sits behind one REST surface at www.wixapis.com. The developer portal is fully public, the reference is machine readable (append .md to any docs URL), and both the orders side and the catalogue side have first-party JavaScript SDKs. For a seller-side commerce database this is one of the easier platforms: orders, line items, fulfillments, products, variants, inventory per location and webhooks are all documented with real samples.
At a glance
What it is
Wix Stores is the commerce module of the Wix site platform. It is D2C only: there is no marketplace, no shared buyer pool, and every store is the merchant's own domain. Wix publishes a general eCommerce layer (/ecom/v1/..., shared by Stores, Bookings, Restaurants and Events) plus a Stores-specific catalogue layer (/stores/v3/...). Wix Headless lets a merchant keep Wix as the commerce backend while serving the storefront from their own frontend, over the same REST API.
Two catalogue generations are in production at once. Catalog V1 (/stores/v1/products plus the V1 inventory endpoints) is legacy; Catalog V3 (/stores/v3/products, /stores/v3/inventory-items) is current. A site is on one or the other, and Wix publishes Get Catalog Version so an integration can branch. Check catalogue version first.
API access
Three credential routes, and the choice decides the whole credential store design.
- Account API key. An account owner or co-owner generates a key in the Wix dashboard with a chosen scope set and a chosen site scope (all sites in the account, or named sites only). Long-lived, valid until revoked or rotated. Right for a server-to-server pipeline pulling one merchant's own data. Not available to third-party Wix apps.
- Wix app (OAuth). Register an app in the Wix app dashboard, request scopes, merchants install it. The app mints short-lived tokens per installed site using
client_credentialsplus the site'sinstance_id. This is the route for a multi-merchant connector on the Wix App Market. - Headless OAuth client. A headless project has a built-in OAuth client with its own ID and secret. It belongs to exactly one site, so no
instance_idis needed.
Official SDKs: JavaScript and TypeScript (@wix/sdk, @wix/ecom, @wix/stores). No first-party server SDK for other languages, so a Python or Go connector calls REST directly. Versions sit in the path (/ecom/v1, /stores/v3); Wix deprecates individual fields with a dated note rather than versioning the whole surface, for example order taxSummary and line item taxDetails, both dated for removal on 30 September 2024 and superseded by taxInfo.
Authentication
API key
- Generate the key in the Wix dashboard as account owner or co-owner, selecting scopes and target sites.
- Send the key verbatim in the
Authorizationheader. Wix's own examples do not use aBearerprefix. - A key is not bound to a site, so every request must identify its target:
wix-site-idfor site-level calls orwix-account-idfor account-level calls. Send one, never both. Site-level calls only work with a key generated from the account that owns the site.
OAuth client credentials
- Generate a client secret (app secret for an app, headless client secret for a headless project).
- POST to the token endpoint with
grant_type: client_credentials. A third-party app also sendsinstance_id, the per-site installation identifier Wix includes in every event and in the app instance query parameter. - The access token is valid for 4 hours (14400 seconds). There is no refresh token for this grant; mint a new one with the same credentials.
- Send the token in
Authorizationon every call.
curl -X POST 'https://www.wixapis.com/oauth2/token' \
-H 'Content-Type: application/json' \
-d '{"grant_type":"client_credentials","client_id":"<CLIENT_ID>","client_secret":"<CLIENT_SECRET>"}'
# returns {"access_token":"...","token_type":"Bearer","expires_in":14400}
curl -X GET \
'https://www.wixapis.com/ecom/v1/fulfillments/orders/265c7d98-a7c3-48c4-89cd-bbdc00921eab' \
-H 'Content-Type: application/json' \
-H 'Authorization: <ACCESS_TOKEN>'
Multi-account: with API keys, one key per Wix account plus wix-site-id to pick the store. With an app, one credential pair and one token per instance_id, so the credential store keys on instance rather than on site.
REST cannot authenticate on behalf of a Wix dashboard user. That identity exists only in the JavaScript SDK via dashboard.auth(). A server-side connector always runs as an app instance or as an API key admin.
Objects we can read
Orders
POST https://www.wixapis.com/ecom/v1/orders/search for bulk pulls, GET /ecom/v1/orders/{orderId} for one. Scope SCOPE.DC-STORES.READ-ORDERS.
The request body wraps everything in a search object holding filter, sort and cursorPaging. Filterable fields include createdDate and updatedDate (with $gt, $gte, $lt, $lte), status, paymentStatus, fulfillmentStatus, channelInfo.type, buyerInfo.email, number and priceSummary.total.amount. Pagination is cursor based: cursorPaging.limit defaults to and caps at 100. Default sort is createdDate descending.
Two behaviours matter for an incremental sync. Search Orders does not return PENDING or REJECTED orders unless you filter for them explicitly, and it never returns INITIALIZED orders, which must be fetched by ID. Drive the window off updatedDate so status changes are picked up.
A request body is {"search": {"filter": {"updatedDate": {"$gte": "2026-09-01T00:00:00.000Z"}}, "sort": [{"fieldName": "updatedDate", "order": "ASC"}], "cursorPaging": {"limit": 100}}}.
Trimmed response, using the documented Order object field names:
{
"orders": [{
"id": "7001d34b-11a6-4a34-8746-dc8ababeca42", "number": 10025,
"createdDate": "2026-09-14T08:12:03.117Z", "updatedDate": "2026-09-15T11:02:44.901Z",
"status": "APPROVED", "paymentStatus": "PAID", "fulfillmentStatus": "PARTIALLY_FULFILLED",
"currency": "USD", "taxIncludedInPrices": false,
"buyerInfo": { "contactId": "f2bcb0cc-...", "email": "buyer@example.com" },
"priceSummary": {
"subtotal": { "amount": "100.00", "formattedAmount": "$100.00" },
"shipping": { "amount": "10.00" }, "tax": { "amount": "8.80" },
"discount": { "amount": "0.00" }, "total": { "amount": "118.80" }
},
"billingInfo": {
"address": { "country": "US", "subdivision": "US-NY", "city": "Brooklyn", "postalCode": "11201", "addressLine": "35 Front St" },
"contactDetails": { "firstName": "Ada", "lastName": "L", "phone": "+15555550123" }
},
"channelInfo": { "type": "WEB" },
"businessLocation": { "id": "3a5f0f6e-...", "name": "Main warehouse" },
"balanceSummary": { "paid": { "amount": "118.80" }, "refunded": { "amount": "0.00" } }
}],
"metadata": { "count": 1, "cursors": { "next": "<cursor>" }, "hasNext": true }
}
billingInfo, recipientInfo and shippingInfo.logistics.shippingDestination all carry PII (name, phone, street address).
Wix returns a field only when it has a value. An order where the buyer skipped the province simply has no billingInfo.address.subdivision key rather than a null. Treat every optional field as absent by default.
Order items
Line items are embedded in the order as lineItems[], not a separate endpoint. Per line: id, productName.original and .translated, catalogReference.catalogItemId with catalogReference.appId (the Wix Stores app ID is always 215238eb-22a5-4c36-9e7b-e7c08025e04e), quantity, decimalQuantity.value for fractional lines, physicalProperties.sku and .weight, price, priceBeforeDiscounts, totalDiscount, totalPriceBeforeTax, totalPriceAfterTax, taxInfo.taxAmount and taxInfo.taxRate, refundQuantity, restockQuantity, itemType, paymentOption, fulfillerId and locations[] with per-location quantity.
Products and listings
Catalog V3: POST /stores/v3/products/search and /query, GET /stores/v3/products/{productId}, GET /stores/v3/products/slug/{slug}. Search takes filter, sort (max 10 rules), cursorPaging (limit max 100) and a free-text search block with expression, fields (including variantsInfo.variants.sku), mode and fuzzy.
Core fields: id, name, slug, visible, productType (PHYSICAL, DIGITAL, SERVICE), actualPriceRange.minValue.amount and .maxValue.amount, inventory.availabilityStatus, variantsInfo.variants[]. Variant data is not returned by Search Products; use Get Product, or the Read-Only Variants V3 API (POST /stores/v3/variants/query and /search) to page variants directly. Reading non-visible products needs SCOPE.STORES.PRODUCT_READ_ADMIN. Catalog V1 sites use POST /stores/v1/products/query instead.
Inventory
Catalog V3: POST /stores/v3/inventory-items/query and /search, GET /stores/v3/inventory-items/{id}. One inventory item is one variant at one location, and the variantId plus locationId pair is unique.
Fields: id, revision, createdDate, updatedDate, variantId, locationId, productId, trackQuantity, availabilityStatus (IN_STOCK, OUT_OF_STOCK, PREORDER), and a one-of pair where either quantity (integer, -99999 to 99999, may go negative when a paid order decrements it) or inStock (boolean, untracked stock) is present. preorderInfo carries enabled, message, limit, counter and remaining preorder quantity. A read-only product block gives name, variantName, variantSku and variantVisible, which is what lets you join inventory to your own SKU without a second call.
{
"id": "a83212bc-1c1e-4645-b391-a9fed7f930f6", "revision": "2",
"createdDate": "2025-12-11T09:27:22.000Z", "updatedDate": "2025-12-11T09:27:24.000Z",
"variantId": "0f0b0e1a-...", "productId": "b1f3d0a2-...", "locationId": "3a5f0f6e-...",
"trackQuantity": true, "quantity": 106, "availabilityStatus": "IN_STOCK",
"product": { "name": "Dark roast 1kg", "variantSku": "COF-DR-1000", "variantVisible": true }
}
Shipments and tracking
GET https://www.wixapis.com/ecom/v1/fulfillments/orders/{orderId}. A fulfillment is the shipment record.
{
"orderWithFulfillments": {
"orderId": "7001d34b-11a6-4a34-8746-dc8ababeca42",
"fulfillments": [{
"id": "00a7eba6-059e-430c-9f8e-9d3d31dd5e9d",
"createdDate": "2026-09-15T11:51:48.233Z",
"lineItems": [{ "id": "00000000-0000-0000-0000-000000000001", "quantity": 1 }],
"trackingInfo": { "trackingNumber": "28674", "shippingProvider": "dhl",
"trackingLink": "https://www.logistics.dhl/global-en/home/tracking.html?tracking-id=28674" },
"status": "In_Delivery", "completed": false
}]
}
}
shippingProvider is either a predefined value (fedex, ups, usps, dhl, canadaPost), in which case Wix generates trackingLink itself, or any custom string, in which case you must supply trackingLink. status is one of Pending, Accepted, Ready, In_Delivery, Fulfilled and is informational: the order's fulfillmentStatus is derived from fulfilled line item quantities, not from this field. There is no carrier scan history, so shipments.events[] cannot be populated from Wix.
Returns, refunds and settlements
Wix has no returns object and no settlement or payout report. Cancellation is POST /ecom/v1/orders/{orderId}/cancel plus order status CANCELED. Refunds must be reconstructed from per-line refundQuantity and restockQuantity, from balanceSummary (balance, paid, refunded, authorized, pending, chargeback, chargebackReversal, platformFees, totalMinusPlatformFees), and from order activities[] entries such as REFUND_INITIATED, PAYMENT_REFUNDED, ORDER_REFUNDED and CHARGEBACK_CREATED. Per-order fees are in platformFeeSummary (total, totalPassOn, totalAbsorbed, and per fee name, amount, lineItemId, percentageRate). Payout timing comes from the payment provider, not from Wix.
Customers and locations
buyerInfo.contactId points into the Contacts API (/contacts/v4/contacts) for name, emails, phones and addresses. The order itself carries buyerInfo.email plus full contact blocks in billingInfo.contactDetails and recipientInfo.contactDetails. All PII, not masked. There are two location identifier spaces. Business locations sit under /locations and appear on an order as businessLocation.id. Stores locations sit under Catalog V3 (GET /stores/v3/locations/{id}, POST /stores/v3/locations/query) and are what inventoryItem.locationId refers to. Keep both.
Writing back: listings, price and stock
All writes are single REST calls with a synchronous result. There is no feed or file mechanism.
Writes are optimistic-concurrency controlled: every update must carry the current revision, and a stale one returns HTTP 409, "entity has already changed since the requested revision". Read the entity, take its revision, then write. Never take a revision from a webhook event, because events can be delayed.
curl -X PATCH \
'https://www.wixapis.com/stores/v3/inventory-items/a83212bc-1c1e-4645-b391-a9fed7f930f6' \
-H 'Content-type: application/json' \
-H 'Authorization: <ACCESS_TOKEN>' \
-d '{"inventoryItem":{"id":"a83212bc-1c1e-4645-b391-a9fed7f930f6","revision":"1","quantity":106},"reason":"MANUAL"}'
reason is ORDER, MANUAL or REVERT_INVENTORY_CHANGE and surfaces on the Inventory Item Updated With Reason event, which is how you tell a connector-driven change apart from a sale. Two documented precondition failures return HTTP 428: enabling preorder on a digital or subscription product, and setting a quantity that drops below the preorder counter or decreases an already negative quantity. Stock writes need SCOPE.STORES.INVENTORY_ITEM_WRITE. Documented size caps: 300 line items per fulfillment, 100 modifier entries per line item. Maximum batch sizes for the product and inventory bulk endpoints are not published.
Webhooks and notifications
Webhooks are an app extension: register a callback URL per event in the Wix app dashboard, and merchants who install the app get the events for their site. An API key alone does not give you webhooks, so a key-based pipeline must poll.
Event bodies share one envelope: id, entityFqdn (for example wix.ecom.v1.order), slug (for example created), entityId, eventTime, and a per-slug block such as createdEvent.entity carrying the full object. Order events: Order Created, Order Updated, Order Approved, Order Committed, Order Canceled, Order Fulfilled, Payment Status Updated, Order Imported, Imported Order Deleted. Catalogue events: Product Created, Product Updated, Product Deleted, and the same created/updated/deleted triplet for categories, brands, ribbons and info sections. Inventory events: Inventory Item Created, Inventory Item Updated, Inventory Item Deleted, Inventory Item Stock Status Updated, Inventory Item Updated With Reason. The minimum useful set for an analytics pipeline is Order Created, Order Updated, Payment Status Updated, Order Canceled, Product Updated and Inventory Item Updated With Reason.
The signature verification scheme and the retry schedule were not readable from the public pages fetched here: the webhook extension articles returned 404 on the markdown mirror. Treat the delivery envelope as verified and verification plus retry as unverified, and confirm them in the app dashboard before relying on them. Without webhooks, poll Search Orders on updatedDate every 5 to 15 minutes and Search Inventory Items on updatedDate hourly.
Rate limits and pagination
Wix does not publish numeric rate limits. The troubleshooting article states only that a 429 means you have been throttled for sending too many requests in a short period and that the remedy is to wait a minute and retry. No quota headers are documented. Treat the limit as unknown, back off exponentially on 429, and keep a conservative sustained rate (a few requests per second per site) until a real limit is observed.
Pagination is uniformly cursor based across the V3 and eCommerce surfaces: send cursorPaging.limit (max 100) and follow metadata.cursors.next until hasNext is false. Cursors are opaque and must not be combined with a changing filter mid-walk, so for a backfill page by ascending createdDate and let a restart resume from the last seen timestamp rather than a dead cursor.
Mapping to the unified model
Gaps and open questions
- Rate limits are not published; the only documented signal is a 429 and a suggestion to wait a minute. Webhook signature verification and retry policy could not be confirmed either, because the webhook extension articles returned 404 on the markdown mirror.
- No returns object, no settlement or payout object and no carrier tracking event history. Returns must be derived and the derivation is lossy (refund amounts and restock quantities, but no structured return reason); delivery timestamps must come from the carrier.
- Catalog V1 and Catalog V3 sites coexist, so a connector needs two catalogue read paths behind Get Catalog Version.
- The Search Orders response sample above is assembled from the documented Order object schema and the Search Orders reference rather than copied from one published example. Field names are from the reference, values are illustrative.
Sources
- Order Object
- Search Orders
- Orders API menu and the Order Created webhook
- List Fulfillments For Single Order
- REST API Authentication, Authentication Methods and Create Access Token
- Make Admin API Calls with Client Credentials
- Search Products (Catalog V3)
- Inventory Item Object and Update Inventory Item (Catalog V3)
- Stores API menu, covering Catalog V1, Catalog V3 and Stores Locations V3
- Troubleshooting, covering 409, 428 and 429 behaviour