Skip to content

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 as ems, flash, kex or spx_pickup. It is a string, not a number. See Shipping types.
  • products: at least one line. Each line names its product by product_variant_id or sku, never both.
  • recipient_address or recipient_address_id: exactly one. Only name is required, but a shipping label needs telephone, address1, sub_district, district, province and postal_code. postal_code is 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 ​

FieldWhat it does
channelWhere the sale came from, such as sales_page or line. Shown as ช่องทางการขาย in the app and its sales reports.
sender_nameSender shown on the label. Defaults to the store's name.
sender_address_idWhich store address the parcel ships from. Leave it out to use the primary address. See Shipping from the right place.
ship_before_payAllow shipping before payment (จัดส่งก่อนชำระ).
auto_request_xshippingDefault 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_timeWhen the customer ordered on your side (epoch ms).
expiration_timeWhen 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_id is 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_id is 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:

FieldNew orderDone
order_state109 confirmed181–184 cancelled
payment_state111 awaiting payment119 paid in full
shipping_state121 nothing shipped129 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.

XSelly Open Platform API v1 · Webhooks v2