Appearance
Create and track orders
An order created through the API is created exactly as if it had been keyed into the XSelly app: stock is reserved, the shipping label can be printed and, unless you opt out, a tracking number is requested from the courier automatically.
The minimum
json
{
"shipping_type": "ems",
"recipient_address": { "name": "คุณทดสอบ ระบบ", "telephone": "0556789201", "address1": "51/102", "sub_district": "บางปะกง", "district": "บางปะกง", "province": "ฉะเชิงเทรา", "postal_code": 24130 },
"products": [{ "product_variant_id": "1984193", "qty": 1 }]
}shipping_type: a courier key such asems,flash,kexorspx_pickup. It is a string, not a number. See Shipping types.products: at least one line. Each line names its product byproduct_variant_idorsku, never both.recipient_addressorrecipient_address_id: exactly one. Onlynameis required, but a shipping label needstelephone,address1,sub_district,district,provinceandpostal_code.postal_codeis a number (24130).
Every other field is optional. They are all listed in the API Reference.
Naming a product by SKU
json
"products": [
{ "product_variant_id": "1984193", "qty": 1, "price": 1220.50 },
{ "sku": "FID3-000", "qty": 2 }
]sku is matched exactly, including case, spacing and punctuation, among the store's live products.
XSelly does not force SKUs to be unique within a store. A SKU used by more than one variant is refused with a 400, and the error lists the product_variant_ids that share it, ready to copy:
json
{
"error": "invalid_request",
"error_description": "products[0].sku: sku \"FID3-000\" is shared by 2 products (product_variant_id \"1984193\", \"1984210\"); send one of those product_variant_id values instead"
}Amounts
discount is a positive number taken off the order: "discount": 15 means ฿15 off, not ฿15 added. shipping_fee and other_fee are charges and are added on. So a ฿500 item with
json
{ "shipping_fee": 40, "other_fee": 10, "discount": 15 }comes back as "total_amount": "535.00" (500 + 40 + 10 − 15).
A negative discount, shipping_fee or other_fee is rejected with 400.
Cash on delivery
Set is_cod: true and send both cod_fee (the COD service fee you charge the customer) and cod_amount (what the courier collects):
json
{
"shipping_type": "spx_pickup",
"is_cod": true,
"cod_fee": 30,
"cod_amount": 1250.50
}With is_cod: true, a plain courier key switches to that courier's COD form, so ems becomes ems_cod. You may also send the _cod key directly, but then is_cod must be true. A plain key with is_cod: true is a 400 when the courier has no COD form. The Shipping types table shows which couriers have one.
Other useful fields
| Field | What it does |
|---|---|
channel | Where the sale came from, such as sales_page or line. Shown as ช่องทางการขาย in the app and its sales reports. |
sender_name | Sender shown on the label. Defaults to the store's name. |
sender_address_id | Which store address the parcel ships from. Leave it out to use the primary address. See Shipping from the right place. |
ship_before_pay | Allow shipping before payment (จัดส่งก่อนชำระ). |
auto_request_xshipping | Default true: request a tracking number from the courier as soon as the order is ready to ship (payment has been confirmed and completed or flag ship_before_pay is true). Send false to supply the tracking number yourself. |
shipping_label_note | หมายเหตุบนใบปะหน้า: printed on the label, so the courier and customer see it. |
private_note | บันทึกช่วยจำ: seen only by the store. |
order_time | When the customer ordered on your side (epoch ms). |
expiration_time | When an unpaid order expires (epoch ms). |
ship_deadline_time | วันกำหนดส่ง: the date the order must ship by (epoch ms). |
Retrying safely
Send an external_order_id (your own order id, up to 128 characters) and retries are safe. The first call creates the order. Every later call with the same external_order_id returns that same order instead of creating a second one. So if a request times out, or your network drops the response, simply send it again.
Without an external_order_id there is nothing to recognise a retry by, and a repeated request creates a second order.
The id only has to be unique within your app. Two apps of the same store may each use "1001".
Following an order: POST /v1/order/detail
Read an order by exactly one of:
json
{ "order_id": "4536645" }json
{ "external_order_id": "SALEPAGE-10231" }The two ids are looked up in different scopes:
order_idis looked up store-wide, so you can also read orders keyed into the XSelly app or pulled from a marketplace. That lets you reconcile a whole day, not only your own orders.external_order_idis looked up within your app only.
Anything outside that scope answers the same 404 not_found: another store's order, another app's external id, a deleted order or an id that never existed.
Create and detail return the same order object, so one parser handles both. open_platform_channel_id is present only on orders created through the API, which is how you tell your own orders from the rest.
Getting the tracking number
A freshly created order has no shipments, even one that asked for a tracking number: XShipping books it moments later. Poll POST /v1/order/detail until it appears:
json
"shipments": [
{
"id": "912233",
"tracking_number": "SPXTH046123456789",
"shipping_type": "spx_pickup",
"products": [{ "product_variant_id": "1984193", "qty": 1 }],
"create_time": 1789470000000
}
]An order that goes out in several parcels has several shipments. A cancelled shipment stays in the list with its cancel_time, because the tracking number it used may still appear on a courier's report.
XShipping issues tracking numbers for the major couriers: ems, ecopost, kex, flash, jt, spx_pickup and spx_dropoff. For the others, the order is created with that shipping type and the store supplies the tracking number itself.
Order, payment and shipping state
Three numbers describe where an order is:
| Field | New order | Done |
|---|---|---|
order_state | 109 confirmed | 181–184 cancelled |
payment_state | 111 awaiting payment | 119 paid in full |
shipping_state | 121 nothing shipped | 129 everything shipped |
The matching timestamps (payment_complete_time, shipping_complete_time, complete_time and cancel_time) appear when each milestone is reached. The full lists are in Codes and constants.
