Skip to content

Conventions ​

These rules hold for every field of every endpoint, and for the webhook payloads too. Build your parser around them once and it works everywhere.

IdentifiersAlways JSON strings, in requests as well as responses.
TimestampsUnix epoch milliseconds, on fields ending in _time.
MoneyA decimal string in responses. A number or a numeric string in requests.
Absent vs nullOmitting an optional field and sending null mean the same thing.
Unknown fieldsRejected with 400.
Request idEvery response carries an X-Request-Id header. Successful ones also repeat it in the body as request_id.

Identifiers ​

Order ids, product variant ids and address ids are all written "4536645", never 4536645. A bare number is rejected with a 400.

json
{ "products": [{ "product_variant_id": "1984193", "qty": 1 }] }

The value is made of digits, but treat it as an opaque string. Store it as text, and send back exactly what we gave you.

Why strings? Every id we hand back is a string, so the simplest integration is also the correct one: read an id from one response and send it straight into the next request. Quoting ids also stops a JavaScript client from rounding a large one. And with one spelling instead of two, neither side ever has to ask which form it is looking at.

Timestamps ​

Every field ending in _time is a Unix timestamp in milliseconds: 1789463270000, not 1789463270 and not "2026-09-15T10:00:00+07:00".

A value in seconds is rejected rather than silently read as a date in 1970.

js
const orderTime = Date.now()                  // JavaScript: already milliseconds
python
order_time = int(time.time() * 1000)          # Python: seconds → milliseconds

Money ​

Responses always send amounts as decimal strings, such as "1280.50", so that no JSON parser rounds them. Parse them with a decimal type when you do arithmetic.

In requests you may send either a number (1280.5) or a numeric string ("1280.50").

Absent vs null ​

Leaving an optional field out and sending it as null mean the same thing. In responses, a field with no value is usually left out rather than sent as null. Check whether a key exists, not only whether it equals null.

Unknown fields are rejected ​

A request with a key the endpoint does not know is refused with 400 invalid_request, naming the key.

This is deliberate. If a misspelled key were quietly ignored, it could create a wrong order that then ships: say, one missing the COD amount you thought you sent. We would rather tell you.

Webhooks work the other way round. You should ignore fields you do not know, because we add new ones over time. See Versioning.

Request id ​

Every response, errors included, carries an X-Request-Id header. A successful response also repeats it in the body as request_id:

json
{ "request_id": "req_01K5A7QW8ZP3RN4MB6C0YEXV2D", "order": { "...": "..." } }

Log it with every call. Quote it when you ask us about a call: it lets us find that exact request. You may also send your own X-Request-Id header, and we will use yours.

No store_id ​

No request has a store_id. Your access token identifies your app, and your app belongs to one store. There is no way to reach another store, and no way to get it wrong.

Why every call is a POST ​

The read endpoints (/v1/order/detail, /v1/product/list and the others) are POSTs too, so that ids and search terms travel in the body. An external_order_id is a string you chose. Put in a query string, it would spread through access logs, proxies and browser history, and you would have to escape it correctly on every call.

XSelly Open Platform API v1 · Webhooks v2