Wish.com

Wish Merchant API v3: OAuth 2.0 authorization code with 37 scopes, reads orders, products, variations and payments, writes price and inventory.

Wish is a cross border marketplace built on very low priced, mostly China-sourced goods sold into North America and Europe through a discovery-driven mobile app rather than search. It was operated by ContextLogic from 2010 and sold to the Korean group Qoo10 in 2024, a change the API itself reflects in a qoo10:read scope. The Merchant API is on version 3, and it is the most conventional design in this catalogue: OAuth 2.0 authorization code, bearer tokens, a consistent {message, code, data} envelope, a genuine sandbox, cursor-style pagination on every collection, partial responses through a fields parameter, and webhooks with a published topic list. If you have integrated Stripe or Shopify, this will feel familiar. The two things to plan for are that inventory is per warehouse inside the variation object with no standalone inventory endpoint, and that Wish publishes no numeric rate limit at all, only the headers that tell you where you stand.

At a glance

Note

merchant.wish.com serves its reference documentation from a JavaScript application, confirmed on 2026-09-21. The material below comes from Wish's own machine readable specification, wish-marketplace-v3-openapi.json, which Wish publishes at merchant.wish.com/api/openapi_spec/get and which carries Wish's own contact addresses in its info block. It was read through the mirror at api-evangelist/wish. Every path, parameter, schema field and enumeration quoted here is from that specification, not reconstructed. The specification contains no response examples, so field tables appear where a sample would normally go.

What it is

Wish launched in 2010 as a wishlist app and became a marketplace optimised for impulse discovery: an infinite feed of very cheap goods with long shipping times, rather than a search box. At its peak it was among the most downloaded shopping apps worldwide. Revenue and active buyers fell sharply from 2021, and in April 2024 ContextLogic sold the Wish platform and brand to Qoo10, the Singapore-headquartered Korean e-commerce group. The qoo10:read OAuth scope in the current specification is the visible trace of that integration.

Sellers are overwhelmingly cross border, with China-based merchants the largest group, shipping directly to consumers in the United States and Europe. That shape drives most of the API's unusual features: customs declaration fields on every variation (HS code, declared name, declared value, declared local name, origin country, restricted flags for batteries and liquids), VAT and EPR compliance surfaces for the EU, France and Germany specifically, and a revenue share model where Wish's commission depends on the source region, destination region, product category and shipping type rather than on a flat category rate.

Fulfilment is normally merchant fulfilled with the merchant's own carrier. Fulfilled by Wish (FBW) and Fulfilled by Store (FBS) exist as programmes, along with Wish Express for fast shipping and Advanced Logistics, and each appears in fulfillment_order_types on the order.

API access

There are two application types and the difference is who the app serves, not what it can do.

  1. Private app. A merchant creating an application for their own store. This is the self-serve route and needs no approval beyond having a merchant account.
  2. Public app. An ERP or integrator building an application used by many merchants. This requires applying to become a verified public application. Wish's own documentation directs public app developers to that approval process, so budget for it in any multi-tenant plan.

Either way, the app gets a client id and client secret and registers a redirect URI, and each merchant then grants access through the OAuth redirect.

The sandbox needs no special request. Create an account at sandbox.merchant.wish.com, point the base URL at https://sandbox.merchant.wish.com/api/v3/, and generate test data by opening the unfulfilled orders page, hovering the dropdown next to the "fulfill with csv" button and choosing Generate Orders. Wish states the sandbox is completely contained and cannot affect real users or the merchant account. That is a better testing story than any other marketplace in this catalogue.

Versions: the surface is V3 and every path is prefixed /api/v3/. There is no dated version scheme. Several fields carry an explicit deprecation note in the specification, notably subcategory_id in favour of category_id, statuses in favour of state on the product filter, msrp, and declared_name in favour of customs_declared_name. Prefer the replacements.

Wish does not publish an SDK, but it does publish an OpenAPI 3.0 specification, which means a client can be generated rather than written.

Authentication

Endpoints

Sequence

  1. Redirect the merchant to the authorize URL with your client_id, your registered redirect_uri and the scopes you need.
GET https://merchant.wish.com/v3/oauth/authorize
  ?client_id={client_id}
  &redirect_uri=https://connectors.example.com/wish/callback
  &response_type=code
  &scope=orders:read orders:write products:read products:write merchant:read payments:read webhook:read webhook:write
  &state={nonce}
  1. The merchant approves and Wish redirects back with a code.

  2. Exchange it. This is POST /api/v3/oauth/access_token and is the only operation in the whole specification with no security requirement.

curl -X POST "https://merchant.wish.com/api/v3/oauth/access_token" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "{code}",
    "client_id": "{client_id}",
    "client_secret": "{client_secret}",
    "redirect_uri": "https://connectors.example.com/wish/callback"
  }'

All five body fields are required. grant_type is the literal string authorization_code.

  1. Send the token on every subsequent call:
Authorization: Bearer 3d6e2d71bc464e42bfbdb081afb73c1e
  1. GET /api/v3/oauth/refresh_token renews it, and GET /api/v3/oauth/test is a cheap liveness check for a stored token, which is the right thing to call at the top of a sync run.

Token lifetime and refresh token lifetime are not stated in the specification. Read them from the token response at runtime rather than hardcoding a value.

Scopes

Thirty-seven scopes, paired read and write. The ones a commerce database needs: orders:read, orders:write, products:read, products:write, merchant:read, payments:read, returns:read, webhook:read, webhook:write, ratings:read, penalties:read. The rest cover ProductBoost advertising, tickets, infractions, compliance, FBW, FBS, videos, notifications, announcements, WishParcel and qoo10:read. Every operation in the specification declares exactly the scope it needs, so a least-privilege grant is straightforward.

Response envelope

Every response has the same three top level attributes.

Check code, not the HTTP status. Wish-Request-Id is returned on every response and is the identifier Wish asks for in support requests, so log it.

Cross-cutting conventions

Three features apply to nearly every endpoint and are worth building into the client once.

Partial responses. Every GET except the OAuth ones accepts a fields parameter. Comma separates top level fields, a forward slash selects a nested field, parentheses select several nested fields at once and * is a wildcard. ?fields=title,items(id,info/name) returns only those. On an object as large as a Wish order this materially reduces both bandwidth and parse cost.

Timestamps. YYYY-MM-DDTHH:mm:ss.sss±XX:ZZ or with a trailing Z for UTC. Milliseconds are optional on input and default to zero. Note that created_at_min and created_at_max on the product filter accept only second precision while updated_at_min and updated_at_max accept millisecond precision; anything more precise is rejected.

Locale. Send Accept-Language to translate content and error messages; the response carries Content-Language with the locale actually used.

Objects we can read

Orders

GET /api/v3/orders (scope orders:read). Filters: limit, states as an array, updated_at_min and updated_at_max, released_at_min and released_at_max, sort_by (updated_at or released_at), and order_type from WISH_EXPRESS, FULFILLED_BY_WISH, FULFILLED_BY_STORE, LESS_THAN_TRUCKLOAD, ADVANCED_LOGISTICS, WISH_PLUS.

Order states: APPROVED (ready to ship), SHIPPED, REFUNDED, REQUIRE_REVIEW (under Wish review, no merchant action) and DECLINED. Note that there is no delivered state at order level; delivery confirmation appears in fulfillment_records.

GET /api/v3/orders/{id} returns a single order. POST /api/v3/orders/bulk_get starts an asynchronous batch download and GET /api/v3/orders/bulk_get/{id} collects the result, which is the right tool for a backfill.

The specification publishes no response example. The order schema is:

Sample response not published.

The commercially important part is order_payment.general_payment_details, and it distinguishes six different prices where most marketplaces have two:

payment_total is the number that matters for the unified model, and original_product_price is the number to use for gross merchandise value. Do not treat any single field as "the price".

order_payment.rev_share explains Wish's commission for this specific order: rev_share as a decimal (0.15 meaning 15 percent), merchant_commission_fees as an amount, and the four inputs that determined it, source_region, destination_region, entity_region, product_category and shipping_type. This is far more transparent than most marketplaces and is worth persisting in full.

order_payment also carries carrier_subsidies[] and shipping_reimbursements[].

Order items

There are no order items. A Wish order is a single variation. product_information names one product and one variation, and general_payment_details.product_quantity gives the count. A buyer who orders three different products generates three orders that share a transaction_id. Model that explicitly: a unified order per Wish order and exactly one order_item inside it, then use transaction_id if you need the basket view.

Products and listings

GET /api/v3/products (scope products:read). Filters: parent_sku, limit, created_at_min and created_at_max (second precision), updated_at_min and updated_at_max (millisecond precision), sort_by with desc returning most recently updated first, state, and the deprecated statuses which defaults to PENDING_REVIEW plus APPROVED.

GET /api/v3/products/{id} returns one product. POST /api/v3/products/bulk_get plus GET /api/v3/products/bulk_get/{id} is the asynchronous batch read.

The product (ShallowProduct) schema: id, name, description, parent_sku, category_id, brand_id, tags[], condition, max_quantity, msrp (deprecated), state, status, unit, reference_value, num_sold, num_saves, is_promoted, category_experience_eligibility, california_prop65_warning_type, california_prop65_chemical_names[], created_at, updated_at. state distinguishes live listings; status distinguishes APPROVED products available for sale from removed ones, which cannot be updated. Sample response not published.

Variations are where the sellable detail lives:

logistics_details is the customs block and is mandatory reading for a cross border catalogue: weight in grams, length, width and height in centimetres, origin_country, customs_hs_code, customs_declared_name, declared_local_name, customs_declared_value, pieces, and restricted_flags[] marking batteries, liquids and similar. declared_name is present but deprecated in favour of customs_declared_name.

Reference data: GET /api/v3/products/categories and /categories/{id}, GET /api/v3/products/attributes, GET /api/v3/brands, GET /api/v3/products/variations/colors, GET /api/v3/currencies.

Inventory

There is no inventory endpoint. Stock is variations[].inventories[] on the product, an array of {warehouse_id, inventory}. That is the only representation, on both read and write, and it means a stock sync is a product read and a product write rather than a dedicated call.

GET /api/v3/merchant/warehouses (scope merchant:read) lists the warehouses whose ids appear there, and POST on the same path and PUT /api/v3/merchant/warehouses/{id} manage them.

Two webhook topics exist specifically for stock, PRODUCT_INVENTORY_CHANGE_MERCHANT and PRODUCT_INVENTORY_CHANGE_WISH_USER, plus PRODUCT_SOLDOUT. Subscribing to those is much cheaper than polling the catalogue.

Shipments and tracking

No shipment object. tracking_information[] on the order carries tracking_number, shipping_provider.name, origin_country and ship_note, ordered newest first and limited to two sets, so a reshipment replaces rather than appends indefinitely. There are no carrier scan events; the nearest thing is fulfillment_records[], whose CONFIRM_FULFILLED and CONFIRMED_DELIVERED entries give confirmation timestamps, and TRACKING_CANCELLED which flags a tracking number Wish rejected.

GET /api/v3/orders/shipping_carriers returns the carriers Wish accepts for a given order_type and dest_country_code, where the destination country is only required for non-GENERAL order types. Call it before shipping; submitting an unaccepted carrier is a common failure.

Returns and cancellations

Wish models these as refunds rather than returns. refunds[] on the order carries refund_amount, merchant_responsible_amount (the portion charged to the merchant, which is the number that matters commercially), refund_reason, refund_reason_note, refund_source and refund_time. There is no separate return list endpoint in the Marketplace V3 specification, although returns:read and returns:write scopes exist, which suggests a returns surface outside this specification. GET /api/v3/orders/{id}/refund_reasons lists the valid reasons for a given order.

Cancellation is the same operation as refunding: PUT /api/v3/orders/{id}/refund.

Payments and settlements

POST /api/v3/payments/invoices/bulk_get starts an asynchronous job and GET /api/v3/payments/invoices/bulk_get/{id} retrieves it. That is the merchant invoice, which is Wish's settlement document. GET /api/v3/payments/early_payment covers the early payment programme.

Per-order economics, though, do not need the invoice: order_payment already carries payment_total, rev_share with merchant_commission_fees, carrier_subsidies[] and shipping_reimbursements[], so a fee ledger can be built from orders alone and reconciled against invoices.

GET /api/v3/penalties, /penalties/{id} and /penalties/count return merchant penalties, and the order's penalties[] links them back. Penalties are a real cost line on Wish, so treat them as part of settlement rather than as an operational afterthought.

Customers

No customer object and no customer id. The buyer appears only as the shipping address on the order, plus customer_identity_number and its type in tax_information, which for some destinations is a taxpayer or customs identifier. transaction_id groups orders placed together, which is the closest thing to a basket, but it is not a durable buyer identity. Treat Wish as offering no customer data.

GET /api/v3/ratings/products and /ratings/products/{id} return product ratings, and GET /api/v3/tickets, PUT|GET /api/v3/tickets/{id} and POST /api/v3/tickets/{id}/replies handle customer support tickets.

Locations

GET /api/v3/merchant/warehouses (scope merchant:read) is the locations source, and warehouse_id there joins to variations[].inventories[].warehouse_id and to warehouse_information.warehouse_id on the order. GET /api/v3/merchant/account_details, /merchant/settings, /merchant/currency_settings and PUT|GET /api/v3/merchant/shipping_settings complete the merchant profile.

Writing back: listings, price and stock

Price and stock

There is no price endpoint and no stock endpoint. Both are attributes of the variation, and both are written by updating the product.

PUT /api/v3/products/{id} (scope products:write) takes an UpdateProduct body whose variations[] array holds up to 400 UpdateProductVariation entries. Each entry requires an id and may carry status (ENABLED or DISABLED), price, cost, sku, gtin, image, options, attributes, quantity_value, logistics_details and inventories.

{
  "variations": [
    {
      "id": "5d30f60e0a6fc464f4f376b0",
      "status": "ENABLED",
      "price": { "amount": 12.99, "currency_code": "USD" },
      "inventories": [
        { "warehouse_id": "5f1a2b3c4d5e6f7a8b9c0d1e", "inventory": 250 },
        { "warehouse_id": "5f1a2b3c4d5e6f7a8b9c0d1f", "inventory": 40 }
      ]
    }
  ]
}

Send price for a revenue-share merchant and cost for a cost-based merchant; the specification marks each as required for its own merchant type. The product-level status is a separate ENABLED or DISABLED flag from the per-variation one, so a listing can be disabled wholesale or variant by variant.

POST /api/v3/products/bulk_update is the asynchronous form. The body is an array of BulkUpdateProduct, which is UpdateProduct plus a required id, so one call can touch many products. It returns a job, and GET /api/v3/products/bulk_update/{id} reports its status. Use the bulk form for scheduled syncs and the single PUT for reactive single-SKU updates.

Listing content

POST /api/v3/products creates a product. Required: name, description, parent_sku, default_shipping_prices, main_image and variations. Optional editable fields include category_id, brand_id, tags, condition, max_quantity, unit, reference_value, attributes, video, consignment, warehouse_to_shippings, msrp (deprecated) and the California Proposition 65 warning fields.

POST /api/v3/products/{id}/variations adds a variation to an existing product. DELETE /api/v3/products/{id} removes a product. PUT /api/v3/products/{id}/calculated_shipping manages calculated shipping. GET /api/v3/products/requests and /products/requests/{id} track product creation and edit requests, which is how a newly created product's review outcome is discovered; a variation create also returns a request_id for the same purpose.

Fulfilment

PUT /api/v3/orders/{id}/tracking (scope orders:write) is the ship action. Required: tracking_number, origin_country as a two-letter ISO code and shipping_provider from the accepted carriers list. ship_note is an optional private note. The same endpoint updates tracking on an already-shipped order.

PUT /api/v3/orders/{id}/refund refunds or cancels, taking a required refund_reason plus an optional refund_reason_note shown to the buyer. PUT /api/v3/orders/{id}/address updates the destination address, which is how the ORDER_ADDRESS_CHANGE webhook is acted on. PUT /api/v3/orders/{id} updates the order itself.

Webhooks and notifications

Subscriptions are created through the API, with no console step.

POST https://merchant.wish.com/api/v3/webhook/subscriptions?topic_id={topic_id}&endpoint=https://connectors.example.com/wish/hook
Authorization: Bearer {token}

GET /api/v3/webhook/topics (scope webhook:read) returns the available topics and their ids. GET, PUT and DELETE on /api/v3/webhook/subscriptions and /subscriptions/{id} list and manage subscriptions. Writes need webhook:write.

The 25 published topics:

ORDER_ALL is the catch-all and is the pragmatic subscription for a commerce database, with ORDER_RELEASE as the new-order signal and ORDER_FULFILLMENT_DEADLINE as the operational alarm. PRODUCT_INVENTORY_CHANGE_WISH_USER firing means a buyer consumed stock, which is the event that keeps an external inventory master honest without polling.

Wish delivers by HTTP POST to the subscribed endpoint. Signature verification, retry policy and delivery guarantees are not documented in the specification. The specification defines the subscription management operations but not the delivery contract. Treat the endpoint as unauthenticated until confirmed with Wish, use an unguessable path, and verify anything received against the API before acting on it.

Rate limits and pagination

Wish publishes no numeric rate limit. The stated contract is per merchant, per app, and the only instrumentation is response headers.

Exceeding the limit returns HTTP 429 and, in Wish's own words, there is nothing to do but wait for the reset. Wish's documented recommendations are to cache responses and to use webhooks instead of polling. Build the client to read Wish-Rate-Limit-Remaining on every response and throttle adaptively from it, since there is no static number to configure.

Pagination is uniform and cursor-like without being a true cursor. Collections take limit plus {attribute}_min and {attribute}_max, where the attribute is id, created_at, updated_at or similar depending on the endpoint, combined with sort_by={attribute}.{asc|desc}. To page: request limit=20&created_at_max=T&sort_by=created_at.desc, take the last object's timestamp, and pass it as the next created_at_max.

One trap, stated explicitly in the specification: id_min and id_max are inclusive, so when paging by id you must add or subtract 1 from the last object's id rather than reusing it, or you will re-read a row every page. Ids are either integers or BSON object ids, which are hexadecimal, so the arithmetic has to handle both.

Asynchronous jobs exist for the heavy operations: orders/bulk_get, products/bulk_get and products/bulk_update all submit and then poll a job id. Use them for backfills and scheduled syncs, and keep the synchronous endpoints for reactive work.

A workable cadence: subscribe to ORDER_ALL, PRODUCT_INVENTORY_CHANGE_WISH_USER and PRODUCT_SOLDOUT; poll GET /api/v3/orders by updated_at_min every 15 minutes as a safety net with a fields parameter trimming the response to what you store; refresh the catalogue with products/bulk_get nightly and GET /api/v3/products by updated_at_min hourly; pull payment invoices weekly; and call GET /api/v3/oauth/test before each run to catch a revoked grant early.

Mapping to the unified model

orders

order_items

One row per order. order_item_id can be {order_id}:{variation_id} or simply the order id, sku is product_information.sku, channel_sku and listing_id are product_information.variation_id with product_information.id as the parent product, title is product_information.name with color and size alongside, quantity is general_payment_details.product_quantity, unit_price is product_merchant_payment (or discounted_price for the buyer-facing figure), tax comes from the order's tax block and status mirrors the order state.

listings

inventory

sku from variation sku, location_id from inventories[].warehouse_id, quantity_available from inventories[].inventory. quantity_reserved and quantity_inbound are not published for merchant-fulfilled stock; for FBW, warehouse_information.fbw_information on the order is the nearest source. updated_at falls back to the product's timestamp.

shipments, returns and settlements

shipments: derive from the order. shipment_id has no native value, so key on {order_id}:{tracking_number}. awb from tracking_information[].tracking_number, carrier from tracking_information[].shipping_provider.name, shipped_at from the MARK_SHIPPED fulfilment record and delivered_at from CONFIRMED_DELIVERED. events[] is fulfillment_records[], which is milestones rather than carrier scans and carries no location. weight_grams comes from the variation's logistics_details.weight, which is already grams. service_level from fulfillment_order_types. shipping_cost and ndr_reason are not available, though TRACKING_CANCELLED is a useful failure signal.

returns: return_id has no native value, so key on {order_id}:{refund_time}. order_id from the order, reason from refund_reason with refund_reason_note, refund_amount from refund_amount with merchant_responsible_amount kept separately because they differ whenever Wish absorbs part of the cost, and initiated_at from refund_time. status and received_at have no equivalent; a Wish refund is a single event, not a state machine.

settlements: from the payment invoice job, with order_id joining back through the order. Per-order fees are better taken from order_payment: fees[] becomes {type: "commission", amount: merchant_commission_fees} plus rows for carrier subsidies and shipping reimbursements, gross_amount is original_product_price, net_amount is payment_total, and penalties from penalties[] resolved through GET /api/v3/penalties/{id} should be added as negative fee rows.

products: sku from parent_sku, gtin from the variation's gtin, brand from brand_id resolved through /api/v3/brands, category from category_id, attributes from attributes[], images from the product's main and extra images. hsn_code maps to logistics_details.customs_hs_code, which is the HS code the unified field was named after.

customers: cannot be populated. locations: from GET /api/v3/merchant/warehouses, with channel_location_id set to warehouse_id.

Gaps and open questions

  • No numeric rate limits. Only headers. The client has to be adaptive rather than configured, and there is no way to size a backfill in advance.
  • Webhook delivery is undocumented. No signature scheme, no retry policy and no ordering guarantee appear in the specification. Until Wish confirms otherwise, webhook bodies cannot be trusted as authentication and every event needs a confirming read.
  • Token lifetimes are unstated. Read expires_in from the token response; do not assume.
  • No response examples anywhere in the specification. Field names and types are exact but the actual shape of a populated order has to be discovered in the sandbox. That is the first thing to do with a sandbox account.
  • OrderShippingDetail is almost empty in the specification, resolving to an OrderAddress reference plus name and neighborhood. The address field list could not be enumerated from the specification alone.
  • A returns surface exists but is not in this specification. returns:read and returns:write scopes are defined with no operations using them. Ask Wish whether a separate returns API exists.
  • processing_status is documented as empty until the V3 PUT endpoints ship, and that note has been in the specification long enough to be stale. Verify whether it is now populated.
  • The Qoo10 transition. A qoo10:read scope exists with no operations using it in this specification. Whether the Wish API will be merged into or replaced by a Qoo10 surface is not addressed anywhere public, and it is the largest medium-term risk to a Wish connector. Worth asking before committing engineering time.
  • Sandbox availability. An independent probe of sandbox.merchant.wish.com in July 2026 returned HTTP 503. Confirm the sandbox is actually up before planning a test strategy around it.

Sources

  • Wish Marketplace V3 OpenAPI 3.0 specification (wish-marketplace-v3-openapi.json), published by Wish at merchant.wish.com/api/openapi_spec/get, carrying Wish's own partner-api@wish.com and marketplace-external-api@contextlogic.com contacts. Read in full: all 74 paths, 245 schemas, the OAuth security scheme with its 37 scopes, and the info.description covering the response envelope, partial responses, date format, locale, rate limiting, sorting, pagination and the sandbox.
  • merchant.wish.com/documentation/api/v3/reference, the official reference portal. Confirmed on 2026-09-21 to render client side and return no documentation to a plain fetch.
  • api-evangelist/wish, an independent third-party profile of Wish's public API surface, used as the mirror for the specification above and for its derived authentication, rate limit, sandbox and webhook topic summaries. The repository states plainly that it is not operated by or affiliated with Wish.