{
  "openapi": "3.2.1",
  "info": {
    "title": "XSelly Open Platform API",
    "version": "1.0.0",
    "summary": "Create and read XSelly orders from your own system.",
    "description": "Work with your XSelly store from your own system — a sale page, a chat bot,\nan ERP.\n\nAn order created through this API is created exactly as if it had been keyed\ninto the XSelly app: stock is reserved, the shipping label can be printed,\nand — unless you opt out — a tracking number is requested from the courier\nautomatically.\n\nNew here? Start with the [Tutorial](/tutorial/), which walks through a first order end to end. This reference lists every endpoint and field, and the **Webhooks** section at the end describes the requests XSelly sends to your server.\n\n## Conventions that hold everywhere\n\n- **Identifiers are always JSON strings**, in requests as well as responses.\n  Order ids, product variant ids and address ids are all written\n  `\"4536645\"`, never `4536645`. A bare number is rejected with a `400`. The\n  value is digits, but treat it as an opaque string: store it as text, and\n  send back exactly what we gave you. One spelling in both directions means\n  an id read from one response can go straight into the next request, and no\n  JavaScript client ever rounds a large one.\n- **Timestamps are unix epoch milliseconds**, on every field suffixed\n  `_time`. Never a formatted date, never seconds — a seconds-precision value\n  is rejected rather than silently read as a date in 1970.\n- **Money is a decimal string in responses** (`\"1280.50\"`), so that no JSON\n  parser rounds it. In requests either a number or a numeric string is\n  accepted.\n- **An absent field and a `null` one mean the same thing.**\n- **Unknown fields are rejected** with a `400`. A misspelled key that was\n  quietly ignored would create a *wrong* order that then ships — one missing\n  the COD amount you thought you sent, say — so we would rather tell you.\n- **Every response carries a `request_id`**, also returned as the\n  `X-Request-Id` header. Quote it when you ask us about a call. Send your\n  own `X-Request-Id` and we will use yours.\n- **No request body has a `store_id`.** Your access token identifies your\n  channel, and your channel decides which store you are working in, so there\n  is no way to reach another store and no way to get it wrong.\n",
    "contact": {
      "name": "XSelly Open Platform support",
      "url": "https://www.xselly.com"
    },
    "license": {
      "name": "Proprietary — for XSelly Open Platform partners"
    }
  },
  "servers": [
    {
      "url": "{base_url}",
      "description": "Your base URL, shown in the XSelly app. Ask the XSelly team if you cannot find it.",
      "variables": {
        "base_url": {
          "default": "https://your-base-url",
          "description": "Your base URL, shown in the XSelly app."
        }
      }
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Exchange your channel's credentials for an access token."
    },
    {
      "name": "Orders",
      "description": "Create an order and follow its payment and shipping progress."
    },
    {
      "name": "Store",
      "description": "Your store's own settings — today, the addresses you ship from."
    },
    {
      "name": "Products",
      "description": "Your store's products, in the shape a sale page draws them."
    },
    {
      "name": "Webhooks",
      "description": "Requests XSelly sends to **your** webhook URL. Acknowledge each with a\n2xx within one second; there are no retries.\n"
    }
  ],
  "paths": {
    "/oauth/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "createAccessToken",
        "summary": "Get an access token",
        "description": "Every `/v1` call carries an access token. Get one with the OAuth2\n**client credentials** grant, using the `client_id` and `client_secret`\nissued for your channel. Credentials go in the form body; HTTP Basic\nclient authentication is not accepted.\n\n**Cache the token and reuse it for its full four hours**\n(`expires_in` is 14400 seconds), refreshing shortly before it expires or\nwhen a call answers `401`. A correct integration needs about six token\ncalls a day.\n\nThe endpoint is rate limited to 10 requests per minute per `client_id`\nand 60 per minute per IP; over that it answers `429` with `slow_down`\nand a `Retry-After` header. Minting a token before every API call will\nhit this. Ten failed authentications within five minutes block that\n`client_id` and that IP for 15 minutes, so never retry a rejected secret\nin a loop.\n\nTreat the token itself as opaque — a string with a lifetime. Two calls\nnever return the same value.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A fresh access token.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                },
                "examples": {
                  "token": {
                    "summary": "A newly minted token",
                    "value": {
                      "access_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
                      "token_type": "Bearer",
                      "expires_in": 14400
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`grant_type` was not `client_credentials`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unsupportedGrantType": {
                    "value": {
                      "error": "unsupported_grant_type",
                      "error_description": "only client_credentials is supported"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Credentials rejected. Every authentication failure answers the same\n`invalid_client`, whether the `client_id` is unknown, the secret is\nwrong, or the channel has expired — by design, so the endpoint\nreveals nothing about which clients exist.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalidClient": {
                    "value": {
                      "error": "invalid_client",
                      "error_description": "client authentication failed"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait out the `Retry-After` header.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before trying again.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "slowDown": {
                    "value": {
                      "error": "slow_down",
                      "error_description": "too many token requests; retry after the Retry-After header"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/order/create": {
      "post": {
        "tags": [
          "Orders"
        ],
        "operationId": "createOrder",
        "summary": "Create one order",
        "description": "Creates one order in the store your channel belongs to.\n\nSend an `external_order_id` — your own order id — and retries become\nsafe: the first call creates the order, and every later call carrying\nthe same `external_order_id` returns *that same order* rather than\ncreating a second one. Without it there is nothing to recognise a retry\nby, and a repeated request creates a second order.\n\nA freshly created order has no `shipments` yet, even one that asked for\na tracking number: XShipping books it moments later. Poll\n`POST /v1/order/detail` for it.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateOrderRequest"
              },
              "examples": {
                "codOrder": {
                  "summary": "A cash-on-delivery order from a sale page",
                  "description": "`sender_address_id` is left out, so the parcel is sent from\nthe store's primary address.\n",
                  "value": {
                    "external_order_id": "SALEPAGE-10231",
                    "shipping_type": "spx_pickup",
                    "is_cod": true,
                    "cod_fee": 30,
                    "cod_amount": 1250.5,
                    "channel": "sales_page",
                    "recipient_address": {
                      "name": "คุณทดสอบ ระบบ",
                      "telephone": "0556789201",
                      "address1": "51/102 บางปะกง",
                      "sub_district": "บางปะกง",
                      "district": "บางปะกง",
                      "province": "ฉะเชิงเทรา",
                      "postal_code": 24130
                    },
                    "products": [
                      {
                        "product_variant_id": "1984193",
                        "qty": 1,
                        "price": 1220.5
                      }
                    ],
                    "shipping_fee": 30,
                    "order_time": 1789463270000
                  }
                },
                "bySkuAndCachedAddresses": {
                  "summary": "Lines named by SKU, shipped from a cached branch address",
                  "description": "`sender_address_id` and `recipient_address_id` are both ids\nthis integration already had on file, so the order is one\ncall.\n",
                  "value": {
                    "external_order_id": "ERP-2026-0009",
                    "shipping_type": "ems",
                    "sender_address_id": "3710939",
                    "recipient_address_id": "3753465",
                    "products": [
                      {
                        "sku": "FID3-000",
                        "qty": 2
                      }
                    ],
                    "shipping_fee": 40,
                    "discount": 15
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The order, as created.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderEnvelope"
                },
                "examples": {
                  "created": {
                    "$ref": "#/components/examples/CreatedOrder"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/order/detail": {
      "post": {
        "tags": [
          "Orders"
        ],
        "operationId": "getOrderDetail",
        "summary": "Read one order",
        "description": "Reads one order. Use it to follow an order's payment and shipping\nprogress, and to pick up the tracking number once the courier has issued\none.\n\nName the order by **exactly one** of `order_id` and `external_order_id`.\nSending both, or neither, is a `400`.\n\nThe two ids differ in scope on purpose. `order_id` is store-wide, so you\ncan also read orders keyed into the XSelly app — which is what makes\nthis endpoint useful for reconciling a whole day rather than only your\nown orders. `external_order_id` is resolved within your channel alone,\nbecause two channels of one store may each have an order \"1001\".\n\nIt is a `POST` rather than a `GET` so that the ids travel in a body: an\n`external_order_id` is a string you chose, and putting it in a query\nstring would spread it through access logs, proxies and browser history.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderQuery"
              },
              "examples": {
                "byXSellyId": {
                  "summary": "By the XSelly order id",
                  "value": {
                    "order_id": "4536645"
                  }
                },
                "byYourOwnId": {
                  "summary": "By your own order id",
                  "value": {
                    "external_order_id": "SALEPAGE-10231"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The order as it stands now.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderEnvelope"
                },
                "examples": {
                  "shipped": {
                    "$ref": "#/components/examples/ShippedOrder"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such order, for you. Anything outside your scope — another\nstore's order, another channel's external id, a deleted order, an id\nthat never existed — answers this same `404`, so the endpoint cannot\nbe used to discover which orders exist.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": "not_found",
                      "error_description": "no such order"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/store/address/list": {
      "post": {
        "tags": [
          "Store"
        ],
        "operationId": "listStoreAddresses",
        "summary": "List your store's own addresses",
        "description": "Lists **your store's own addresses** — the ตั้งค่าร้าน → ที่อยู่ร้าน list\nin the XSelly app. These are the addresses a `sender_address_id` can\nname, so call this to learn your ids rather than hard-coding them.\n\n**Do not call this often.** A store's own addresses are its branches and\nwarehouses: they are set up once and then barely change, and the ids\nthey carry never change at all. Call it **once**, cache the\n`sender_address_id` you need on your side, and use the cached value from\nthen on — refreshing only when the addresses actually change in XSelly\n(someone adds a warehouse, moves the primary tick, or a stored id stops\nworking). Creating an order then costs one call instead of two, which is\nmeasurably faster and keeps you well clear of the rate limits; listing\nthe addresses before every order is the most common way to make an\notherwise good integration slow. If you ship from a single place you can\nskip this endpoint altogether and simply leave `sender_address_id` out\nof the create call.\n\nYour customers' addresses are not in this list: those belong to the\nstore's contacts, and this endpoint is about where parcels are sent\n*from*.\n\nAddresses come back primary-first, then oldest-first — the same order\nthe sender fallback uses — so the first address of the first page is the\none an order with no `sender_address_id` is sent from.\n",
        "requestBody": {
          "required": false,
          "description": "Optional. Send nothing at all, `{}`, or a page.\n",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListStoreAddressesRequest"
              },
              "examples": {
                "firstPage": {
                  "summary": "The usual call — no arguments",
                  "value": {}
                },
                "secondPage": {
                  "summary": "A store with more addresses than one page holds",
                  "value": {
                    "limit": 50,
                    "offset": 50
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One page of the store's own addresses.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListStoreAddressesResponse"
                },
                "examples": {
                  "twoAddresses": {
                    "value": {
                      "request_id": "req_01K5A9F3T7Q2WPRB8N0MZDXCV4",
                      "addresses": [
                        {
                          "id": "3653293",
                          "is_primary": true,
                          "name": "คลังสินค้าหลัก",
                          "telephone": "0898765432",
                          "address1": "142/108 ถ.กาญจนาภิเษก",
                          "sub_district": "บางแค",
                          "district": "บางแค",
                          "province": "กทม",
                          "postal_code": 10160
                        },
                        {
                          "id": "3710939",
                          "is_primary": false,
                          "name": "สาขาใหม่",
                          "telephone": "0599777882",
                          "address1": "11/11 บ้านนี้ดี อยู่แล้วรวย",
                          "sub_district": "บางปะกง",
                          "district": "บางปะกง",
                          "province": "ฉะเชิงเทรา",
                          "postal_code": 24130,
                          "legal_entity_type": 11,
                          "legal_entity_id": "1011544012007",
                          "branch_type": "h"
                        }
                      ],
                      "has_more": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/product/list": {
      "post": {
        "tags": [
          "Products"
        ],
        "operationId": "listProducts",
        "summary": "List your store's products",
        "description": "Lists **your store's products** — the same list as สินค้า in the XSelly\napp — in the small shape a sale page needs to draw a product card: an\nid, a name and a picture.\n\n**Variants, prices and stock are not in it.** A product can carry a\nhundred variants, so the list stays one row per product; call\n`POST /v1/product/detail` for the product a buyer opens.\n\nTo walk the whole catalogue, keep adding `limit` to `offset` while\n`has_more` is `true`; send `get_count: true` once, on the first page, if\nyou want a total. If the store edits its products while you page, one\ncan move between pages; sort by `id` when you need every product\nexactly once.\n",
        "requestBody": {
          "required": false,
          "description": "Optional. Send nothing at all, `{}`, or any of the fields below.\n",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListProductsRequest"
              },
              "examples": {
                "firstPage": {
                  "summary": "The first page, most recently updated first",
                  "value": {}
                },
                "search": {
                  "summary": "A name search, alphabetical, with a total",
                  "value": {
                    "limit": 20,
                    "offset": 0,
                    "query": "หมวก",
                    "sort_by": "name",
                    "sort_order": "asc",
                    "get_count": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One page of the store's products.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListProductsResponse"
                },
                "examples": {
                  "withCount": {
                    "value": {
                      "request_id": "req_01K5A9F3T7Q2WPRB8N0MZDXCV4",
                      "products": [
                        {
                          "id": "569041",
                          "name": "ชุดนอน เนื้อผ้านุ่มลื่น ใส่สบาย",
                          "image_url": "https://p16-oec-sg.ibyteimg.com/tos-alisg-i-aphluv4xwc-sg/df89e619900848eda999920401dfc210~tplv-aphluv4xwc-origin-jpeg.jpeg"
                        },
                        {
                          "id": "569040",
                          "name": "SS ชุดเด็ก 3 ชิ้น พร้อมหมวกสุดเท่",
                          "image_url": "https://cf.shopee.co.th/file/sg-11134283-8259r-mti2amlllrlu1f"
                        }
                      ],
                      "total": 975,
                      "has_more": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/product/detail": {
      "post": {
        "tags": [
          "Products"
        ],
        "operationId": "getProductDetail",
        "summary": "Read one product in full",
        "description": "Reads **one product in full** — pictures, description, category, and\nevery variant with its SKU, prices per price tier and stock per\nwarehouse. Each variant's `product_variant_id` is what\n`products[].product_variant_id` takes when creating an order.\n\nThe id is looked up within your store; anything outside it answers the\nsame `404`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductQuery"
              },
              "examples": {
                "byId": {
                  "value": {
                    "product_id": "569032"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The product.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductDetailEnvelope"
                },
                "examples": {
                  "oneVariant": {
                    "value": {
                      "request_id": "req_01K5A9F3T7Q2WPRB8N0MZDXCV4",
                      "product": {
                        "id": "569032",
                        "name": "หมวกกันแดด ผ้าฝ้ายโพลีเอสเตอร์ ระบายอากาศดี",
                        "description": "หมวกกันแดด ผลิตจากผ้าฝ้ายโพลีเอสเตอร์ ...",
                        "store_id": "3",
                        "img_uris": [
                          "https://cf.shopee.co.th/file/th-11134207-81ztf-mtho943byolfed"
                        ],
                        "thumbnail_uris": [
                          "https://cf.shopee.co.th/file/th-11134207-81ztf-mtho943byolfed"
                        ],
                        "tiny_img_uris": [
                          "https://cf.shopee.co.th/file/th-11134207-81ztf-mtho943byolfed"
                        ],
                        "create_time": 1790226380000,
                        "update_time": 1790226523000,
                        "price_tier_ids": [
                          "1138"
                        ],
                        "warehouse_ids": [
                          "30629"
                        ],
                        "shipping_rates": [],
                        "min": "799.00",
                        "max": "799.00",
                        "variants": [
                          {
                            "product_variant_id": "1984307",
                            "name": "Yellow",
                            "create_time": 1790226380000,
                            "update_time": 1790226380000,
                            "img_url": "https://cf.shopee.co.th/file/sg-11134253-8260g-mk3zylt21wclcf",
                            "sku": "AB20-01",
                            "prices": [
                              {
                                "id": "2833534",
                                "price_tier_id": "1138",
                                "price": "799.00"
                              }
                            ],
                            "cost": "0.00",
                            "available_qty": 12,
                            "on_hand": 15,
                            "reserved_qty": 3,
                            "warehouses": [
                              {
                                "wh_id": "30629",
                                "on_hand": 15,
                                "available_qty": 12,
                                "reserved_qty": 3
                              }
                            ],
                            "weight": 250
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such product in your store. Another store's product, a deleted\none and an id that never existed all answer this same `404`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": "not_found",
                      "error_description": "no such product"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The access token from `POST /oauth/token`, sent on every `/v1` call as\n`Authorization: Bearer <access token>`. It identifies your channel, and\nyour channel decides which store you are working in.\n"
      },
      "OAuth2ClientCredentials": {
        "type": "oauth2",
        "description": "The same token, described as an OAuth2 flow for tooling that can fetch\nit for you. Credentials go in the form body; HTTP Basic client\nauthentication is not accepted.\n",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "/oauth/token",
            "scopes": {}
          }
        }
      }
    },
    "headers": {
      "XRequestId": {
        "description": "The id of this call, echoed in the body as `request_id`. Quote it when\nyou ask us about a call. Send your own and we will use yours.\n",
        "schema": {
          "type": "string",
          "examples": [
            "req_01K5A7QW8ZP3RN4MB6C0YEXV2D"
          ]
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Something in the request is wrong. `error_description` names the\noffending field, for example `products[1].qty`. Sending an id as a\nnumber rather than a string lands here.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "unknownCourier": {
                "summary": "An unsupported shipping type",
                "value": {
                  "error": "invalid_request",
                  "error_description": "shipping_type: \"pigeon\" is not a supported courier"
                }
              },
              "numericId": {
                "summary": "An id sent as a number",
                "value": {
                  "error": "invalid_request",
                  "error_description": "1984193 is not an id: ids are sent as a JSON string, e.g. \"1984193\""
                }
              },
              "duplicateSku": {
                "summary": "A SKU worn by more than one variant",
                "value": {
                  "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"
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The access token is missing, malformed or expired. Get a new one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "invalidToken": {
                "value": {
                  "error": "invalid_token",
                  "error_description": "the access token is invalid or has expired"
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Either your channel may not do that for its store (`access_denied`), or\nthe store's subscription order quota is exceeded (`quota_exceeded`),\nwhich only the store owner can resolve.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "accessDenied": {
                "value": {
                  "error": "access_denied",
                  "error_description": "this channel may not create orders for its store"
                }
              },
              "quotaExceeded": {
                "value": {
                  "error": "quota_exceeded",
                  "error_description": "the store's subscription order quota is exceeded"
                }
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body is over the 1 MB limit.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "tooLarge": {
                "value": {
                  "error": "invalid_request",
                  "error_description": "the request body is too large"
                }
              }
            }
          }
        }
      },
      "ServerError": {
        "description": "Our problem. Retry — with an `external_order_id`, retrying a create is\nsafe.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "serverError": {
                "value": {
                  "error": "server_error",
                  "error_description": "the order could not be created"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Id": {
        "type": "string",
        "pattern": "^[0-9]+$",
        "description": "An identifier — an order id, a product variant id, an address id.\n\n**Always a JSON string, in requests as well as responses**: `\"4536645\"`,\nnever `4536645`. A bare number is rejected with a `400`. The value is\ndigits, but treat it as opaque: store it as text and send back exactly\nwhat we gave you.\n",
        "examples": [
          "4536645"
        ]
      },
      "Money": {
        "description": "An amount in baht. Responses always send a decimal **string**\n(`\"1280.50\"`) so that no JSON parser rounds it; requests accept either a\nnumber or a numeric string.\n",
        "oneOf": [
          {
            "type": "string",
            "pattern": "^-?[0-9]+(\\.[0-9]+)?$"
          },
          {
            "type": "number"
          }
        ],
        "examples": [
          "1280.50"
        ]
      },
      "MoneyOut": {
        "type": "string",
        "pattern": "^-?[0-9]+(\\.[0-9]+)?$",
        "description": "An amount in baht, always a decimal string in responses.",
        "examples": [
          "1280.50"
        ]
      },
      "EpochMillis": {
        "type": "integer",
        "format": "int64",
        "description": "A unix timestamp in **milliseconds**. Never a formatted date and never\nseconds — a seconds-precision value is rejected rather than silently\nread as a date in 1970.\n",
        "examples": [
          1789463270000
        ]
      },
      "RequestId": {
        "type": "string",
        "description": "The id of this call, also returned as the `X-Request-Id` header. Quote\nit when you ask us about a call.\n",
        "examples": [
          "req_01K5A7QW8ZP3RN4MB6C0YEXV2D"
        ]
      },
      "Error": {
        "type": "object",
        "description": "The one error shape, shared by the OAuth and the `/v1` endpoints. Branch\non `error`, never on the prose in `error_description`: the wording may\nimprove, the codes will not change.\n",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "The machine-readable code to branch on.",
            "enum": [
              "invalid_request",
              "invalid_client",
              "invalid_token",
              "unsupported_grant_type",
              "slow_down",
              "access_denied",
              "quota_exceeded",
              "not_found",
              "server_error"
            ]
          },
          "error_description": {
            "type": "string",
            "description": "A human-readable explanation, naming the offending field where there\nis one. For people reading logs, not for code to match on.\n"
          }
        }
      },
      "TokenRequest": {
        "type": "object",
        "required": [
          "grant_type",
          "client_id",
          "client_secret"
        ],
        "properties": {
          "grant_type": {
            "type": "string",
            "const": "client_credentials",
            "description": "Always `client_credentials`."
          },
          "client_id": {
            "type": "string",
            "description": "Your channel's client id, `xs_...`.",
            "examples": [
              "xs_9f2c41b8d7e04a15"
            ]
          },
          "client_secret": {
            "type": "string",
            "format": "password",
            "description": "Your channel's secret, `xss_...`. It is shown once when issued and\ncannot be read back, only reset.\n"
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "required": [
          "access_token",
          "token_type",
          "expires_in"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Send it on every `/v1` call as `Authorization: Bearer <token>`.\nTreat it as opaque and cache it for its full lifetime.\n"
          },
          "token_type": {
            "type": "string",
            "const": "Bearer"
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds the token stays valid — four hours. Refresh shortly before\nit runs out, or when a call answers `401`.\n",
            "examples": [
              14400
            ]
          }
        }
      },
      "CreateOrderRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "shipping_type",
          "products"
        ],
        "description": "The body of `POST /v1/order/create`. Exactly one of `recipient_address`\nand `recipient_address_id` is required. Any key not listed here is\nrejected with a `400`.\n",
        "properties": {
          "shipping_type": {
            "$ref": "#/components/schemas/ShippingType"
          },
          "products": {
            "type": "array",
            "minItems": 1,
            "description": "The order lines — at least one.",
            "items": {
              "$ref": "#/components/schemas/CreateOrderProduct"
            }
          },
          "recipient_address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Address"
              }
            ],
            "description": "Where the parcel goes, created with the order. Use this *or*\n`recipient_address_id`, never both.\n"
          },
          "recipient_address_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "An address your store already has, instead of `recipient_address` —\nthe `recipient_address_id` of an earlier order for that customer.\n"
          },
          "external_order_id": {
            "type": "string",
            "maxLength": 128,
            "description": "Your own order id. Stored with the order, and doubles as the\nidempotency key: re-posting one returns the order created the first\ntime rather than a second one. It only has to be unique within your\nchannel.\n",
            "examples": [
              "SALEPAGE-10231"
            ]
          },
          "is_cod": {
            "type": "boolean",
            "default": false,
            "description": "Cash on delivery."
          },
          "cod_fee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "The COD service fee you charge the customer. Required when `is_cod`\nis true, and only accepted then.\n"
          },
          "cod_amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "The amount the courier collects on delivery. Required when `is_cod`\nis true, and only accepted then.\n"
          },
          "sender_name": {
            "type": "string",
            "description": "Sender shown on the shipping label. Defaults to your store's name.\n"
          },
          "sender_address_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "**Optional. If not sent, the primary address will be used as\ndefault.**\n\nWhere the parcel is sent from — one of your store's own addresses,\nwhose id comes from `POST /v1/store/address/list`. Omit it and the\naddress marked ที่อยู่หลัก (primary) is used; if the store has ticked\nnone, your oldest address is. Ship from a single place and you never\nneed to send this field at all.\n\nWhen you do send it, cache the id rather than listing the addresses\nbefore every order.\n"
          },
          "channel": {
            "$ref": "#/components/schemas/SalesChannel"
          },
          "discount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Discount given to the customer, as a **positive** number taken\n*off* the order: `15` means ฿15 off, not ฿15 added. A negative value\nis rejected. Default `0`.\n"
          },
          "shipping_fee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Shipping charged to the customer, added on. Default `0`; a negative\nvalue is rejected.\n"
          },
          "other_fee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Any other charge, added on. Default `0`; a negative value is\nrejected.\n"
          },
          "ship_before_pay": {
            "type": "boolean",
            "default": false,
            "description": "Allow the order to ship before it is paid (จัดส่งก่อนชำระ).\n"
          },
          "auto_request_xshipping": {
            "type": "boolean",
            "default": true,
            "description": "Request a tracking number from the courier as soon as the order is\nready to ship (payment has been confirmed and completed or flag\n`ship_before_pay` is true), where your store has XShipping set up\nfor that courier.\nSend `false` to opt out and supply the tracking number yourself.\n"
          },
          "shipping_label_note": {
            "type": "string",
            "maxLength": 1000,
            "description": "หมายเหตุบนใบปะหน้า — printed on the shipping label, so the courier and\nthe customer both see it.\n"
          },
          "private_note": {
            "type": "string",
            "maxLength": 1000,
            "description": "บันทึกช่วยจำ — only your store sees it, never the customer and never\nthe label.\n"
          },
          "order_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "When the customer placed the order on your side. Defaults to the\ncreation time.\n"
          },
          "expiration_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "When an unpaid order expires."
          },
          "ship_deadline_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "วันกำหนดส่ง — the date the order must be shipped by."
          }
        }
      },
      "CreateOrderProduct": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "qty"
        ],
        "description": "One order line. Name the product by **either** `product_variant_id`\n**or** `sku` — one of the two is required, and sending both is a `400`,\nbecause if they disagree we will not guess which product you meant.\n",
        "properties": {
          "product_variant_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The variant id in your store, as a string (`\"1984193\"`, not\n`1984193`) — the same id this API returns in\n`products[].product_variant_id` and the stock webhook reports as\n`data.items[].id`.\n"
          },
          "sku": {
            "type": "string",
            "description": "That variant's SKU, instead of `product_variant_id`. Matched\n**exactly** — case, spacing and punctuation included — among your\nstore's live products. XSelly does not force SKUs to be unique\nwithin a store, so a SKU worn by more than one variant is refused\nwith a `400` listing the ids that share it; send one of those\ninstead.\n",
            "examples": [
              "FID3-000"
            ]
          },
          "qty": {
            "type": "integer",
            "minimum": 1,
            "description": "How many, at least 1."
          },
          "price": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Unit price to charge. Defaults to your store's own price for that\nvariant. A negative value is rejected.\n"
          }
        }
      },
      "Address": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name"
        ],
        "description": "An address. Only `name` is required, but a shipping label needs\n`telephone`, `address1`, `sub_district`, `district`, `province` and\n`postal_code`. The legal-entity fields are only for a tax invoice.\n",
        "properties": {
          "name": {
            "type": "string",
            "description": "Recipient name. **Required.**",
            "examples": [
              "คุณทดสอบ ระบบ"
            ]
          },
          "telephone": {
            "type": "string",
            "description": "Contact number, as the courier should see it.",
            "examples": [
              "0556789201"
            ]
          },
          "address1": {
            "type": "string",
            "description": "House number and street."
          },
          "address2": {
            "type": "string",
            "description": "Second line, if any."
          },
          "address3": {
            "type": "string",
            "description": "Third line, if any."
          },
          "sub_district": {
            "type": "string",
            "description": "ตำบล / แขวง"
          },
          "district": {
            "type": "string",
            "description": "อำเภอ / เขต"
          },
          "province": {
            "type": "string",
            "description": "จังหวัด"
          },
          "postal_code": {
            "type": "integer",
            "description": "Five digits, as a number (`24130`) — not a string.",
            "examples": [
              24130
            ]
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Customer email."
          },
          "note": {
            "type": "string",
            "description": "Free note kept with the address."
          },
          "legal_entity_id": {
            "type": "string",
            "description": "The number on a tax invoice — a national id or a tax id, depending\non `legal_entity_type`.\n",
            "examples": [
              "1011544012007"
            ]
          },
          "legal_entity_type": {
            "$ref": "#/components/schemas/LegalEntityType"
          },
          "branch_type": {
            "type": "string",
            "enum": [
              "h",
              "b"
            ],
            "description": "`h` = head office (สำนักงานใหญ่), `b` = a branch.\n"
          },
          "branch_number": {
            "type": "string",
            "description": "The branch number, when `branch_type` is `b`."
          }
        }
      },
      "OrderAddress": {
        "allOf": [
          {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Id"
                  }
                ],
                "description": "Reusable as `recipient_address_id` on a later order for the same\ncustomer.\n"
              }
            }
          },
          {
            "$ref": "#/components/schemas/Address"
          }
        ],
        "description": "A stored address — an address plus the id it can be reused by."
      },
      "StoreAddress": {
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "is_primary"
            ],
            "properties": {
              "id": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Id"
                  }
                ],
                "description": "Use it as `sender_address_id` when creating an order — and cache\nit, rather than listing the addresses again.\n"
              },
              "is_primary": {
                "type": "boolean",
                "description": "The ที่อยู่หลัก tick the store owner sets in the XSelly app. The\napp allows several addresses to be ticked, so what really\ndecides the default is the list order: primary first, then\noldest first.\n"
              }
            }
          },
          {
            "$ref": "#/components/schemas/Address"
          }
        ],
        "description": "One of the store's own addresses, in the shape a `sender_address_id`\nnames. Every other field is the same `Address` shape the create request\ntakes, so an address you list can be posted back verbatim; fields the\nstore never filled in are absent rather than `null`.\n"
      },
      "OrderQuery": {
        "type": "object",
        "additionalProperties": false,
        "description": "The body of `POST /v1/order/detail`. Exactly one of the two ids —\nsending both, or neither, is a `400`.\n",
        "minProperties": 1,
        "maxProperties": 1,
        "properties": {
          "order_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The XSelly order id, as a string (`\"4536645\"`) — exactly what create\nreturned as `order.id`. Resolved **within your store**, so you can\nalso read orders keyed into the XSelly app.\n"
          },
          "external_order_id": {
            "type": "string",
            "description": "Your own id, the one you sent at creation. Resolved **within your\nchannel** — two channels of one store may each have an order \"1001\",\nand each sees only its own.\n",
            "examples": [
              "SALEPAGE-10231"
            ]
          }
        }
      },
      "ListStoreAddressesRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Both fields are optional; an absent body lists the first page.\n",
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "default": 100,
            "description": "How many to return. The default already holds every address of a\nnormal store.\n"
          },
          "offset": {
            "type": "integer",
            "minimum": 0,
            "default": 0,
            "description": "How many to skip."
          }
        }
      },
      "ListStoreAddressesResponse": {
        "type": "object",
        "required": [
          "request_id",
          "addresses",
          "has_more"
        ],
        "properties": {
          "request_id": {
            "$ref": "#/components/schemas/RequestId"
          },
          "addresses": {
            "type": "array",
            "description": "Primary first, then oldest first — so the first address of the first\npage is the one an order with no `sender_address_id` is sent from.\n",
            "items": {
              "$ref": "#/components/schemas/StoreAddress"
            }
          },
          "has_more": {
            "type": "boolean",
            "description": "Whether another page exists past this one."
          }
        }
      },
      "ListProductsRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Every field is optional; an absent body lists the first page, most\nrecently updated first.\n",
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 20,
            "description": "How many products to return."
          },
          "offset": {
            "type": "integer",
            "minimum": 0,
            "default": 0,
            "description": "How many to skip."
          },
          "query": {
            "type": "string",
            "description": "Only products whose name contains this, ignoring case.",
            "examples": [
              "หมวก"
            ]
          },
          "sort_by": {
            "type": "string",
            "enum": [
              "update_time",
              "create_time",
              "name",
              "id"
            ],
            "default": "update_time"
          },
          "sort_order": {
            "type": "string",
            "enum": [
              "desc",
              "asc"
            ],
            "default": "desc"
          },
          "get_count": {
            "type": "boolean",
            "default": false,
            "description": "Also return `total`. Counting every match is a query of its own, and\n`has_more` is all you need to page, so ask only when you show it.\n"
          }
        }
      },
      "ListProductsResponse": {
        "type": "object",
        "required": [
          "request_id",
          "products",
          "has_more"
        ],
        "properties": {
          "request_id": {
            "$ref": "#/components/schemas/RequestId"
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            }
          },
          "total": {
            "type": "integer",
            "description": "How many products match the request, across every page. Only\npresent when the request sent `get_count: true`.\n"
          },
          "has_more": {
            "type": "boolean",
            "description": "Whether another page exists past this one."
          }
        }
      },
      "Product": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "description": "One product, as a sale page needs it to draw a card. Variants, prices\nand stock are in `POST /v1/product/detail`.\n",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string"
          },
          "image_url": {
            "type": "string",
            "format": "uri",
            "description": "The product's main picture. Absent when it has none."
          }
        }
      },
      "ProductQuery": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "product_id"
        ],
        "properties": {
          "product_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "A product id from `POST /v1/product/list`."
          }
        }
      },
      "ProductDetailEnvelope": {
        "type": "object",
        "required": [
          "request_id",
          "product"
        ],
        "properties": {
          "request_id": {
            "$ref": "#/components/schemas/RequestId"
          },
          "product": {
            "$ref": "#/components/schemas/ProductDetail"
          }
        }
      },
      "ProductDetail": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "store_id",
          "img_uris",
          "thumbnail_uris",
          "tiny_img_uris",
          "variants"
        ],
        "description": "One product in full. Optional fields the product does not have are\nabsent rather than `null`.\n\nA **pulled product** (สินค้าจากร้านค้าส่ง) is one your store pulled\nfrom a seller store to resell: it carries `parent_id`, and each of its\nvariants carries `pulled_from_product_variant_id`.\n",
        "properties": {
          "id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The product's id."
          },
          "name": {
            "type": "string",
            "description": "The product's name, as the store wrote it."
          },
          "description": {
            "type": "string",
            "description": "The product's description, as the store wrote it. Empty when it has none."
          },
          "store_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The store the product belongs to — always your own store."
          },
          "img_uris": {
            "type": "array",
            "description": "The product's pictures at full size. The first one is the main\npicture. Empty when it has none.\n",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "thumbnail_uris": {
            "type": "array",
            "description": "The same pictures as `img_uris`, thumbnail size, in the same order.",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "tiny_img_uris": {
            "type": "array",
            "description": "The same pictures as `img_uris`, smallest size, in the same order.",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "create_time": {
            "type": "integer",
            "format": "int64",
            "description": "When the product was created, in epoch milliseconds."
          },
          "update_time": {
            "type": "integer",
            "format": "int64",
            "description": "When the product was last changed, in epoch milliseconds."
          },
          "category": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "description": "The product's category (หมวดหมู่). Absent when it has none.",
            "properties": {
              "id": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Id"
                  }
                ],
                "description": "The category's id."
              },
              "name": {
                "type": "string",
                "description": "The category's name."
              },
              "color": {
                "type": "string",
                "description": "The colour the XSelly app shows the category in, e.g. `\"#F5A623\"`."
              }
            }
          },
          "price_tier_ids": {
            "type": "array",
            "description": "The price tiers (ราคาขาย groups) the product is sold in;\n`variants[].prices[].price_tier_id` names one of these.\n",
            "items": {
              "$ref": "#/components/schemas/Id"
            }
          },
          "pg_vd_ids": {
            "type": "array",
            "description": "The volume discounts (ส่วนลดขายส่ง) set on the product, one entry\nper price tier a discount applies to. Absent when there is none.\n",
            "items": {
              "type": "object",
              "required": [
                "price_tier_id",
                "vd_id"
              ],
              "properties": {
                "price_tier_id": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Id"
                    }
                  ],
                  "description": "The price tier the discount applies to — one of `price_tier_ids`."
                },
                "vd_id": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Id"
                    }
                  ],
                  "description": "The volume discount's id."
                }
              }
            }
          },
          "warehouse_ids": {
            "type": "array",
            "description": "The warehouses the product is stocked in;\n`variants[].warehouses[].wh_id` names one of these.\n",
            "items": {
              "$ref": "#/components/schemas/Id"
            }
          },
          "shipping_rates": {
            "type": "array",
            "description": "The shipping rates the store has set for this product in\nparticular. Absent or empty when it has none.\n",
            "items": {
              "$ref": "#/components/schemas/ProductShippingRate"
            }
          },
          "seller_shipping_rates": {
            "type": "array",
            "description": "On a pulled product, the seller store's shipping rates for it.\nAbsent when the seller charges no shipping.\n",
            "items": {
              "$ref": "#/components/schemas/ProductShippingRate"
            }
          },
          "min": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "The lowest variant price across all price tiers."
          },
          "max": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "The highest variant price across all price tiers."
          },
          "parent_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "On a pulled product, the seller store's product it was pulled from.\nAbsent on your store's own products.\n"
          },
          "has_child": {
            "type": "boolean",
            "description": "`true` if the product has been pulled by a reseller — at least one\nother store has pulled it to resell. Absent otherwise.\n"
          },
          "variants": {
            "type": "array",
            "description": "The product's variants. A product sold in a single form has exactly one.",
            "items": {
              "$ref": "#/components/schemas/ProductVariantDetail"
            }
          }
        }
      },
      "ProductShippingRate": {
        "type": "object",
        "required": [
          "id",
          "shipping_type",
          "is_cod",
          "init_qty",
          "init_price",
          "next_price"
        ],
        "description": "`init_price` for the first `init_qty` units, `next_price` for each unit\nafter.\n",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "shipping_type": {
            "type": "string",
            "description": "A shipping type key, the same vocabulary order create takes."
          },
          "is_cod": {
            "type": "boolean"
          },
          "init_qty": {
            "type": "integer"
          },
          "init_price": {
            "$ref": "#/components/schemas/MoneyOut"
          },
          "next_price": {
            "$ref": "#/components/schemas/MoneyOut"
          },
          "create_time": {
            "type": "integer",
            "format": "int64"
          },
          "update_time": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "ProductVariantDetail": {
        "type": "object",
        "required": [
          "product_variant_id",
          "name",
          "available_qty",
          "weight"
        ],
        "description": "One variant — the thing a buyer actually orders. Optional fields the\nvariant does not have are absent rather than `null`.\n\nA variant can be a **set** (สินค้าเซ็ต, `product_bundle_id`): sold as a\nfixed combination of other variants, its `available_qty` worked out\nfrom its components' stock. Or it can be\nan **assembled product** (สินค้าประกอบ, `product_assembly_id`): built\nfrom other variants and holding stock of its own; assembling it uses\nup the components, disassembling returns them.\n",
        "properties": {
          "product_variant_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The variant's id. Send it as `products[].product_variant_id` when\ncreating an order.\n"
          },
          "name": {
            "type": "string",
            "description": "The option the buyer picks, e.g. `\"Yellow\"`. **Empty for a product\nsold in a single form** — show the product's name alone.\n"
          },
          "create_time": {
            "type": "integer",
            "format": "int64",
            "description": "When the variant was created, in epoch milliseconds."
          },
          "update_time": {
            "type": "integer",
            "format": "int64",
            "description": "When the variant was last changed, in epoch milliseconds."
          },
          "img_url": {
            "type": "string",
            "format": "uri",
            "description": "The variant's own picture at full size. Absent when it has none —\nfall back to the product's `img_uris`.\n"
          },
          "thumbnail_url": {
            "type": "string",
            "format": "uri",
            "description": "The variant's picture, thumbnail size."
          },
          "tiny_img_url": {
            "type": "string",
            "format": "uri",
            "description": "The variant's picture, smallest size."
          },
          "sku": {
            "type": "string",
            "description": "The store's own code for the variant. It must be unique within the\nstore; older data can still hold a SKU shared by two variants, and\n`POST /v1/order/create` refuses such a SKU (send\n`product_variant_id` instead). Absent when not set.\n"
          },
          "upc": {
            "type": "string",
            "description": "The variant's barcode (UPC / EAN). Unlike `sku` it can be\nduplicated within the store — several variants may carry the same\none. Absent when not set.\n"
          },
          "pulled_from_product_variant_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "On a pulled product, the seller store's variant this one was pulled\nfrom. Absent on your store's own products.\n"
          },
          "product_bundle_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "Present when the variant is a set (สินค้าเซ็ต) — the id of the set\ndefinition listing its components. Absent otherwise.\n"
          },
          "bundled_pp_count": {
            "type": "integer",
            "description": "On a set, how many different variants it is made of. E.g. a set of\n2 shirts and 1 cap has `bundled_pp_count: 2`.\n"
          },
          "bundled_sum_qty": {
            "type": "integer",
            "description": "On a set, the total number of units across all its components.\nE.g. a set of 2 shirts and 1 cap has `bundled_sum_qty: 3`.\n"
          },
          "bundled_by_count": {
            "type": "integer",
            "description": "How many sets use this variant as a component. Selling any of those\nsets also takes stock from this variant.\n"
          },
          "product_assembly_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "Present when the variant is an assembled product (สินค้าประกอบ) —\nthe id of the assembly definition listing its components. Absent\notherwise.\n"
          },
          "assembled_pp_count": {
            "type": "integer",
            "description": "On an assembled product, how many different variants one unit is\nbuilt from.\n"
          },
          "assembled_sum_qty": {
            "type": "integer",
            "description": "On an assembled product, the total number of component units one\nunit is built from.\n"
          },
          "allow_negative_stock": {
            "type": "boolean",
            "description": "If `true`, the variant can still be reserved (ordered) when its\n`available_qty` is zero, and `available_qty` then goes negative.\nAbsent means `false`.\n"
          },
          "prices": {
            "type": "array",
            "description": "The variant's price in each price tier.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "price_tier_id",
                "price"
              ],
              "properties": {
                "id": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Id"
                    }
                  ],
                  "description": "The id of this price entry."
                },
                "price_tier_id": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Id"
                    }
                  ],
                  "description": "The price tier — one of the product's `price_tier_ids`."
                },
                "price": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/MoneyOut"
                    }
                  ],
                  "description": "The variant's selling price in that tier."
                }
              }
            }
          },
          "cost": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "The store's cost for one unit of the variant."
          },
          "available_qty": {
            "type": "integer",
            "description": "Units that can still be sold (พร้อมขาย): `on_hand` minus\n`reserved_qty`. Can be negative when `allow_negative_stock` is\n`true`. On a pulled product it is the seller's stock.\n"
          },
          "on_hand": {
            "type": "integer",
            "description": "Units physically in stock, across all warehouses. Only on the\nstore's own products.\n"
          },
          "reserved_qty": {
            "type": "integer",
            "description": "Units on the store's orders that are waiting to ship (รอส่ง). Only\non the store's own products.\n"
          },
          "warehouses": {
            "type": "array",
            "description": "Stock per warehouse. Only on the store's own products.",
            "items": {
              "type": "object",
              "required": [
                "wh_id",
                "on_hand",
                "available_qty",
                "reserved_qty"
              ],
              "properties": {
                "wh_id": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Id"
                    }
                  ],
                  "description": "The warehouse — one of the product's `warehouse_ids`."
                },
                "on_hand": {
                  "type": "integer",
                  "description": "Units physically in this warehouse."
                },
                "available_qty": {
                  "type": "integer",
                  "description": "Units in this warehouse that can still be sold."
                },
                "reserved_qty": {
                  "type": "integer",
                  "description": "Units in this warehouse waiting to ship."
                }
              }
            }
          },
          "weight": {
            "type": "integer",
            "description": "The weight of one unit, in grams."
          }
        }
      },
      "OrderEnvelope": {
        "type": "object",
        "required": [
          "request_id",
          "order"
        ],
        "description": "Create and detail return the same envelope and the same `order` object,\nso you write one parser and use it for both.\n",
        "properties": {
          "request_id": {
            "$ref": "#/components/schemas/RequestId"
          },
          "order": {
            "$ref": "#/components/schemas/Order"
          }
        }
      },
      "Order": {
        "type": "object",
        "description": "One order. It has no `store_id`: your access token already decides the\nstore, so an order could never come back from another one. Optional\nfields are absent rather than `null` when the order has nothing to say.\n",
        "required": [
          "id",
          "order_state",
          "payment_state",
          "shipping_state",
          "shipping_type",
          "is_cod",
          "sender_name",
          "total_amount",
          "discount",
          "shipping_fee",
          "other_fee",
          "products",
          "create_time"
        ],
        "properties": {
          "id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The XSelly order id. Send it back as `order_id` to read the order again."
          },
          "open_platform_channel_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The channel that created the order through this API. Absent on an\norder keyed into the XSelly app or pulled from a marketplace — which\nis how you tell your own orders from the rest.\n"
          },
          "external_order_id": {
            "type": "string",
            "description": "Your own id, if you sent one."
          },
          "order_state": {
            "$ref": "#/components/schemas/OrderState"
          },
          "payment_state": {
            "$ref": "#/components/schemas/PaymentState"
          },
          "shipping_state": {
            "$ref": "#/components/schemas/ShippingState"
          },
          "shipping_type": {
            "type": "string",
            "description": "The courier key. Empty if the order was later switched, in the app,\nto a courier this API does not offer.\n"
          },
          "is_cod": {
            "type": "boolean",
            "description": "Whether the COD variant of that courier is in use."
          },
          "channel": {
            "$ref": "#/components/schemas/SalesChannel"
          },
          "sender_name": {
            "type": "string",
            "description": "Sender on the shipping label."
          },
          "sender_address_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The store address the parcel is sent from — the primary one, if you\ndid not name one.\n"
          },
          "recipient_address_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The customer's address id, reusable as `recipient_address_id` on a\nlater order.\n"
          },
          "recipient_address": {
            "$ref": "#/components/schemas/OrderAddress"
          },
          "total_amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "What the customer owes: the lines, plus shipping, COD fee and other\ncosts, minus discounts.\n"
          },
          "discount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "Order-level discount, positive, already subtracted from\n`total_amount`.\n"
          },
          "shipping_fee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "Shipping charged to the customer."
          },
          "other_fee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "Other charges."
          },
          "cod_fee": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "The COD service fee. COD orders only."
          },
          "cod_amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "What the courier collects. COD orders only."
          },
          "products": {
            "type": "array",
            "description": "The order lines.",
            "items": {
              "$ref": "#/components/schemas/OrderProduct"
            }
          },
          "shipments": {
            "type": "array",
            "description": "Shipping records, once there are any. A freshly created order has\nnone, even one that asked for a tracking number — XShipping books it\nmoments later, so poll for it.\n",
            "items": {
              "$ref": "#/components/schemas/Shipment"
            }
          },
          "shipping_label_note": {
            "type": "string",
            "description": "หมายเหตุบนใบปะหน้า, if the order has one."
          },
          "private_note": {
            "type": "string",
            "description": "บันทึกช่วยจำ, if the order has one."
          },
          "create_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "When XSelly created the order."
          },
          "order_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "When the customer placed it on your side."
          },
          "expiration_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "When an unpaid order expires."
          },
          "ship_deadline_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "วันกำหนดส่ง, if the order has one."
          },
          "ready_to_ship_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "พร้อมส่งเมื่อ — when the order entered the packing queue. Absent until\nit does.\n"
          },
          "payment_complete_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "Set once the order is fully paid."
          },
          "shipping_complete_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "Set once everything has shipped."
          },
          "complete_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "Set once the order is closed."
          },
          "cancel_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "Set if the order was cancelled; `order_state` is then 181-184.\n"
          }
        }
      },
      "OrderProduct": {
        "type": "object",
        "required": [
          "product_variant_id",
          "name",
          "variant",
          "qty",
          "price",
          "price_after_discount"
        ],
        "description": "One line of the order, as stored.",
        "properties": {
          "product_variant_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Id"
              }
            ],
            "description": "The variant id, as a string — send this exact value back when you\norder the same variant again.\n"
          },
          "name": {
            "type": "string",
            "description": "Product name.",
            "examples": [
              "รองเท้าผ้าใบ"
            ]
          },
          "variant": {
            "type": "string",
            "description": "Variant name (colour, size, …).",
            "examples": [
              "ชมพู",
              "เบอร์ 28"
            ]
          },
          "sku": {
            "type": "string",
            "description": "The variant's SKU, if it has one."
          },
          "qty": {
            "type": "integer",
            "description": "Quantity ordered."
          },
          "price": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "Unit price charged."
          },
          "price_after_discount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MoneyOut"
              }
            ],
            "description": "Unit price after per-unit discounts."
          }
        }
      },
      "Shipment": {
        "type": "object",
        "required": [
          "id",
          "tracking_number",
          "create_time"
        ],
        "description": "One shipping record. An order ships in more than one when it goes out in\nseveral parcels. A cancelled shipment is kept, with its `cancel_time`,\nbecause the tracking number it burned may still appear on a courier's\nreport.\n",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "tracking_number": {
            "type": "string",
            "description": "The courier tracking number.",
            "examples": [
              "SPXTH046123456789"
            ]
          },
          "shipping_type": {
            "type": "string",
            "description": "Courier key for this shipment, when it is one this API offers.\n"
          },
          "products": {
            "type": "array",
            "description": "What went out in it. A `qty` can be less than the order line's when\nthe order ships in parts.\n",
            "items": {
              "$ref": "#/components/schemas/ShipmentProduct"
            }
          },
          "create_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "When the shipment was recorded."
          },
          "cancel_time": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EpochMillis"
              }
            ],
            "description": "Set if the shipment was cancelled."
          }
        }
      },
      "ShipmentProduct": {
        "type": "object",
        "required": [
          "product_variant_id",
          "qty"
        ],
        "properties": {
          "product_variant_id": {
            "$ref": "#/components/schemas/Id"
          },
          "qty": {
            "type": "integer",
            "description": "How many of that variant went out in this shipment."
          }
        }
      },
      "ShippingType": {
        "type": "string",
        "description": "The courier, named by key — never a number. Combine it with `is_cod`:\n`is_cod: false` (the default) uses the plain key, and `is_cod: true`\nswitches to that courier's cash-on-delivery key (`ems` becomes\n`ems_cod`). Sending a `_cod` key directly works too, but then `is_cod`\nmust be `true`; a plain key with `is_cod: true` is a `400` when the\ncourier has no COD form.\n\nResponses use these same keys plus `is_cod`. An order the store later\nswitched, in the app, to a courier not on this list reads back under\nthat courier's key — you just cannot ask for one.\n\nNot every courier here can be *booked* automatically: XShipping issues\ntracking numbers for the major ones (`ems`, `ecopost`, `kex`, `flash`,\n`jt`, `spx_pickup`, `spx_dropoff`), and for the rest the order is\ncreated with that shipping type and you supply the tracking number\nyourself. `store_front`, `buyer_pickup` and `other` are not couriers at\nall: they record that the customer collects the parcel.\n\nEach value below shows the name the XSelly app gives it under\nรูปแบบจัดส่ง, and the name of its cash-on-delivery form where it has one.\n",
        "examples": [
          "spx_pickup"
        ],
        "enum": [
          "best",
          "best_pickup",
          "buyer_pickup",
          "dhl",
          "dhl_bulky",
          "dhl_pickup",
          "ecopost",
          "ems",
          "ems_world",
          "fast_instant_delivery_pack_2_hrs",
          "fast_instant_delivery_pack_30_mins",
          "fedex",
          "flash",
          "flash_bulky",
          "flash_pickup",
          "fuze",
          "grab",
          "inter",
          "ittransport",
          "jt",
          "jt_cod_pickup",
          "jt_pickup",
          "kex",
          "kex_dropoff",
          "kex_pickup",
          "lalamove",
          "lex",
          "lineman",
          "makesend",
          "nim",
          "normal",
          "other",
          "register",
          "scg",
          "shopee_std_delivery",
          "shopee_std_delivery_bulky",
          "shopee_xpress",
          "shopee_xpress_bulky",
          "skootar",
          "slow_instant_delivery",
          "speedd",
          "spx_dropoff",
          "spx_express_bulky_cod",
          "spx_pickup",
          "store_front",
          "tp",
          "true",
          "undefined",
          "ups",
          "zto"
        ],
        "x-enumDescriptions": {
          "best": "BEST Express · with `is_cod: true`: BEST Express [COD]",
          "best_pickup": "BEST Express [นัดรับ]",
          "buyer_pickup": "ผู้ซื้อรับด้วยตนเอง",
          "dhl": "DHL Express · with `is_cod: true`: DHL Express [COD]",
          "dhl_bulky": "DHL Bulky · with `is_cod: true`: DHL Bulky [COD]",
          "dhl_pickup": "DHL Express [นัดรับ]",
          "ecopost": "ไปรษณีย์ eCo-Post · with `is_cod: true`: ไปรษณีย์ eCo-Post [COD]",
          "ems": "ไปรษณีย์ด่วนพิเศษ (EMS) · with `is_cod: true`: ไปรษณีย์ด่วนพิเศษ (EMS) [COD]",
          "ems_world": "EMS World (ระหว่างประเทศ)",
          "fast_instant_delivery_pack_2_hrs": "ส่งทันที (แพ็ค 2 ชั่วโมง)",
          "fast_instant_delivery_pack_30_mins": "ส่งทันที (แพ็ค 30 นาที)",
          "fedex": "FedEx",
          "flash": "Flash Express · with `is_cod: true`: Flash Express [COD]",
          "flash_bulky": "Flash Bulky · with `is_cod: true`: Flash Bulky [COD]",
          "flash_pickup": "Flash Express [นัดรับ]",
          "fuze": "FUZE · with `is_cod: true`: FUZE [COD]",
          "grab": "Grab",
          "inter": "Inter Express Logistics",
          "ittransport": "IT Transport",
          "jt": "J&T Express · with `is_cod: true`: J&T Express [COD]",
          "jt_cod_pickup": "J&T Express [COD นัดรับ]",
          "jt_pickup": "J&T Express [นัดรับ]",
          "kex": "Kerry Express · with `is_cod: true`: Kerry Express [COD]",
          "kex_dropoff": "Kerry Express [Drop-Off]",
          "kex_pickup": "Kerry Express [นัดรับ]",
          "lalamove": "Lalamove",
          "lex": "Lazada Express · with `is_cod: true`: Lazada Express [COD]",
          "lineman": "Lineman",
          "makesend": "MAKESEND · with `is_cod: true`: MAKESEND [COD]",
          "nim": "NiM Express · with `is_cod: true`: NiM Express [COD]",
          "normal": "ไปรษณีย์ธรรมดา",
          "other": "อื่นๆ",
          "register": "ไปรษณีย์ลงทะเบียน · with `is_cod: true`: ไปรษณีย์ลงทะเบียน [COD]",
          "scg": "SCG Express · with `is_cod: true`: SCG Express [COD]",
          "shopee_std_delivery": "SPX Standard Delivery",
          "shopee_std_delivery_bulky": "SPX Standard Delivery Bulky",
          "shopee_xpress": "SPX Express · with `is_cod: true`: SPX Express [COD]",
          "shopee_xpress_bulky": "SPX Bulky",
          "skootar": "Skootar",
          "slow_instant_delivery": "ส่งด่วน",
          "speedd": "SPEED-D · with `is_cod: true`: SPEED-D [COD]",
          "spx_dropoff": "SPX Express [Dropoff] · with `is_cod: true`: SPX Express [Dropoff COD]",
          "spx_express_bulky_cod": "SPX Bulky [COD] · COD only, send it with `is_cod: true`",
          "spx_pickup": "SPX Express [Pickup] · with `is_cod: true`: SPX Express [Pickup COD]",
          "store_front": "ขายหน้าร้าน",
          "tp": "TP Logistics · with `is_cod: true`: TP Logistics [COD]",
          "true": "True e-Logistics · with `is_cod: true`: True e-Logistics [COD]",
          "undefined": "ไม่ระบุ",
          "ups": "ups (United Parcel Service)",
          "zto": "ZTO Express · with `is_cod: true`: ZTO Express [COD]"
        }
      },
      "SalesChannel": {
        "type": "string",
        "description": "Optional. Tags the order with where the sale came from — the\nช่องทางการขาย shown in the XSelly app and used in its sales reports.\n",
        "examples": [
          "sales_page"
        ],
        "enum": [
          "ai_chat",
          "branch",
          "claim",
          "consignment",
          "dealer",
          "distributor",
          "eleven_street",
          "event",
          "facebook",
          "facebook_page",
          "foodpanda",
          "friend",
          "grab",
          "influencer",
          "instagram",
          "japanesepartner",
          "lazada",
          "line",
          "line_add",
          "line_man",
          "line_shopping",
          "marketing",
          "others",
          "repurchase",
          "resellers_not_yet_use_app",
          "robinhood",
          "sales_page",
          "shop",
          "shopee",
          "shopee_food",
          "shopify",
          "telesales",
          "tiktok",
          "twitter",
          "vrich",
          "website"
        ]
      },
      "LegalEntityType": {
        "type": "integer",
        "description": "What the customer is, and with it what `legal_entity_id` holds. Only\nneeded for a tax invoice, and paired with `branch_type`.\n\nAn **individual**, whose `legal_entity_id` is a national id\n(เลขประจำตัวประชาชน): `1` บุคคลธรรมดา (a person), `3` ห้างหุ้นส่วนสามัญ\n(ordinary partnership), `4` ร้านค้า (a shop), `5` คณะบุคคล (group of\npersons).\n\nA **juristic person**, whose `legal_entity_id` is a tax id\n(เลขประจำตัวผู้เสียภาษี): `11` บริษัทจำกัด (limited company — the common\none), `12` บริษัทมหาชนจำกัด (public limited company), `13` ห้างหุ้นส่วนจำกัด\n(limited partnership), `14` มูลนิธิ (foundation), `15` สมาคม\n(association), `16` กิจการร่วมค้า (joint venture), `19` อื่นๆ (other), and\n`2` นิติบุคคล — a legacy catch-all still stored on older addresses.\n",
        "enum": [
          1,
          2,
          3,
          4,
          5,
          11,
          12,
          13,
          14,
          15,
          16,
          19
        ],
        "examples": [
          11
        ]
      },
      "OrderState": {
        "type": "integer",
        "description": "Where the order sits in its own lifecycle: `101` waiting to be\nconfirmed; `102` confirmed by the buyer, waiting on the seller (reseller\nchains); `103` confirmed by the seller, waiting on the buyer; `109`\n**confirmed** — where an order created through this API starts; `181`\ncancelled by the seller; `182` cancelled by the buyer; `183` cancelled\nby the system; `184` cancelled after expiring unpaid.\n\nAny value from `181` up means cancelled, and `cancel_time` is then set.\n",
        "enum": [
          101,
          102,
          103,
          109,
          181,
          182,
          183,
          184
        ],
        "examples": [
          109
        ]
      },
      "PaymentState": {
        "type": "integer",
        "description": "`111` awaiting payment — where a new order starts; `112` overpaid, more\nwas received than the order is worth; `113` both a payment and a refund\nare open; `114` a payment is recorded and waiting to be confirmed; `115`\na refund is waiting to be confirmed; `119` **paid in full**, with\n`payment_complete_time` set.\n",
        "enum": [
          111,
          112,
          113,
          114,
          115,
          119
        ],
        "examples": [
          111
        ]
      },
      "ShippingState": {
        "type": "integer",
        "description": "`121` nothing shipped yet — where a new order starts; `122` only lines\nsourced from a supplier are left to ship; `129` **everything shipped**,\nwith `shipping_complete_time` set.\n",
        "enum": [
          121,
          122,
          129
        ],
        "examples": [
          121
        ]
      },
      "StockAvailableUpdatedEvent": {
        "type": "object",
        "required": [
          "request_id",
          "event_type",
          "request_time",
          "data"
        ],
        "description": "The webhook envelope, carrying a `stock_available_updated` payload in\n`data`. Timestamp fields use the `*_time` suffix and are unix epoch\n**milliseconds**.\n",
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Unique id of this delivery.",
            "examples": [
              "evt_018f4b3c2a7e7d3ab1c9d2e4f5a6b7c8"
            ]
          },
          "event_type": {
            "type": "string",
            "const": "stock_available_updated",
            "description": "The event."
          },
          "request_time": {
            "type": "integer",
            "format": "int64",
            "description": "When the request was sent (epoch ms).",
            "examples": [
              1718385160415
            ]
          },
          "data": {
            "type": "object",
            "required": [
              "items"
            ],
            "properties": {
              "items": {
                "type": "array",
                "description": "One or more changes — changes are batched.",
                "items": {
                  "$ref": "#/components/schemas/StockAvailableUpdatedItem"
                }
              }
            }
          }
        }
      },
      "StockAvailableUpdatedItem": {
        "type": "object",
        "required": [
          "id",
          "sku",
          "old",
          "new",
          "warehouse_id",
          "update_time",
          "reason"
        ],
        "description": "One change of one variant's available quantity in one warehouse.\n`order_id` and `user_id` are mutually exclusive; which one accompanies a\nreason is listed under `reason`, and either may be absent.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "Your product variant id — the same id the REST API calls\n`product_variant_id`.\n",
            "examples": [
              "456313132"
            ]
          },
          "sku": {
            "type": "string",
            "description": "Variant SKU. May be `\"\"` when the variant has no SKU.",
            "examples": [
              "SHIRT-RED-M"
            ]
          },
          "old": {
            "type": "number",
            "description": "Available quantity before the change.",
            "examples": [
              12
            ]
          },
          "new": {
            "type": "number",
            "description": "Available quantity after the change.",
            "examples": [
              11
            ]
          },
          "warehouse_id": {
            "type": "string",
            "description": "Warehouse the change applies to.",
            "examples": [
              "12345"
            ]
          },
          "update_time": {
            "type": "integer",
            "format": "int64",
            "description": "When the change happened (epoch ms). Sequence deliveries by it.",
            "examples": [
              1718385160123
            ]
          },
          "reason": {
            "$ref": "#/components/schemas/StockChangeReason"
          },
          "order_id": {
            "type": "string",
            "description": "Present only when an order caused the change. Mutually exclusive\nwith `user_id`.\n",
            "examples": [
              "178465431"
            ]
          },
          "user_id": {
            "type": "string",
            "description": "Present only when a user made the change. Mutually exclusive with\n`order_id`.\n",
            "examples": [
              "45431"
            ]
          }
        }
      },
      "StockChangeReason": {
        "type": "string",
        "description": "Why the quantity changed. New reasons may appear at any time — accept\nunknown values.\n\n| reason | cause | actor field |\n|---|---|---|\n| `order_reserved` | new order reserved stock | `order_id` |\n| `order_edited` | product edited in an order | `order_id` |\n| `order_canceled` | order canceled by the buyer/reseller | `order_id` |\n| `order_canceled_by_system` | order canceled by the system | `order_id` |\n| `available_qty_reconciled` | available qty reconciled to remaining stock | `order_id` |\n| `shipping_canceled` | shipment canceled | `order_id` |\n| `variant_created` | variant created | `user_id` |\n| `user_adjusted` | user edited remaining quantity | `user_id` |\n| `user_added` | user manual add | `user_id` |\n| `returned` | return received | `user_id` |\n| `purchased` | purchase received | `user_id` |\n| `deposited` | deposit/refill | `user_id` |\n| `user_deducted` | user manual deduct | `user_id` |\n| `damaged` | damaged stock | `user_id` |\n| `lost` | lost stock | `user_id` |\n| `withdrawn` | withdrawn | `user_id` |\n| `user_set` | user set warehouse quantity | `user_id` |\n| `stock_counted` | stock count | `user_id` |\n| `system_init` | system initialization | — |\n| `admin_edited` | edited by administrator | — |\n| `system_corrected` | system correction | — |\n| `command_edited` | edited by system command | — |\n| `fullfilment_updated` | fulfillment service update | — |\n| `assemble_added` / `assemble_deducted` | product assembly | — |\n| `disassemble_added` / `disassemble_deducted` | product disassembly | — |\n| `bundle_converted` / `bundle_edited` | bundle operations | — |\n| `unknown` | other internal adjustment | — |\n",
        "examples": [
          "order_reserved"
        ],
        "enum": [
          "order_reserved",
          "order_edited",
          "order_canceled",
          "order_canceled_by_system",
          "available_qty_reconciled",
          "shipping_canceled",
          "variant_created",
          "user_adjusted",
          "user_added",
          "returned",
          "purchased",
          "deposited",
          "user_deducted",
          "damaged",
          "lost",
          "withdrawn",
          "user_set",
          "stock_counted",
          "system_init",
          "admin_edited",
          "system_corrected",
          "command_edited",
          "fullfilment_updated",
          "assemble_added",
          "assemble_deducted",
          "disassemble_added",
          "disassemble_deducted",
          "bundle_converted",
          "bundle_edited",
          "unknown"
        ]
      }
    },
    "examples": {
      "CreatedOrder": {
        "summary": "A COD order, moments after creation",
        "description": "No `shipments` yet, even though the order asked for a tracking number —\nXShipping books it moments later.\n",
        "value": {
          "request_id": "req_01K5A7QW8ZP3RN4MB6C0YEXV2D",
          "order": {
            "id": "4536645",
            "open_platform_channel_id": "12",
            "external_order_id": "SALEPAGE-10231",
            "order_state": 109,
            "payment_state": 111,
            "shipping_state": 121,
            "shipping_type": "spx_pickup",
            "is_cod": true,
            "channel": "sales_page",
            "sender_name": "ร้านตัวอย่าง",
            "sender_address_id": "3653293",
            "recipient_address_id": "3753465",
            "recipient_address": {
              "id": "3753465",
              "name": "คุณทดสอบ ระบบ",
              "telephone": "0556789201",
              "address1": "51/102 บางปะกง",
              "sub_district": "บางปะกง",
              "district": "บางปะกง",
              "province": "ฉะเชิงเทรา",
              "postal_code": 24130
            },
            "total_amount": "1280.50",
            "discount": "0.00",
            "shipping_fee": "30.00",
            "other_fee": "0.00",
            "cod_fee": "30.00",
            "cod_amount": "1250.50",
            "products": [
              {
                "product_variant_id": "1984193",
                "name": "รองเท้าผ้าใบ",
                "variant": "ชมพู,เบอร์ 28",
                "sku": "SHOE-PINK-28",
                "qty": 1,
                "price": "1220.50",
                "price_after_discount": "1220.50"
              }
            ],
            "create_time": 1789463270000,
            "order_time": 1789463270000
          }
        }
      },
      "ShippedOrder": {
        "summary": "The same order once it is paid and shipped",
        "description": "`shipments[]` now carries the courier tracking number, and the payment\nand shipping milestones are set.\n",
        "value": {
          "request_id": "req_01K5A8B2M4V7T0XKD9RHFGQ3NZ",
          "order": {
            "id": "4536645",
            "open_platform_channel_id": "12",
            "external_order_id": "SALEPAGE-10231",
            "order_state": 109,
            "payment_state": 119,
            "shipping_state": 129,
            "shipping_type": "spx_pickup",
            "is_cod": true,
            "channel": "sales_page",
            "sender_name": "ร้านตัวอย่าง",
            "sender_address_id": "3653293",
            "recipient_address_id": "3753465",
            "recipient_address": {
              "id": "3753465",
              "name": "คุณทดสอบ ระบบ",
              "telephone": "0556789201",
              "address1": "51/102 บางปะกง",
              "sub_district": "บางปะกง",
              "district": "บางปะกง",
              "province": "ฉะเชิงเทรา",
              "postal_code": 24130
            },
            "total_amount": "1280.50",
            "discount": "0.00",
            "shipping_fee": "30.00",
            "other_fee": "0.00",
            "cod_fee": "30.00",
            "cod_amount": "1250.50",
            "products": [
              {
                "product_variant_id": "1984193",
                "name": "รองเท้าผ้าใบ",
                "variant": "ชมพู,เบอร์ 28",
                "sku": "SHOE-PINK-28",
                "qty": 1,
                "price": "1220.50",
                "price_after_discount": "1220.50"
              }
            ],
            "shipments": [
              {
                "id": "912233",
                "tracking_number": "SPXTH046123456789",
                "shipping_type": "spx_pickup",
                "products": [
                  {
                    "product_variant_id": "1984193",
                    "qty": 1
                  }
                ],
                "create_time": 1789470000000
              }
            ],
            "create_time": 1789463270000,
            "order_time": 1789463270000,
            "payment_complete_time": 1789480000000,
            "shipping_complete_time": 1789479000000,
            "complete_time": 1789480000000
          }
        }
      }
    },
    "parameters": {
      "XXSellySignature": {
        "name": "X-XSelly-Signature",
        "in": "header",
        "required": true,
        "description": "Lowercase hex HMAC-SHA256 of the **raw request body**, keyed with your\nwebhook secret. Recompute it over the exact bytes you received, before\nany JSON parsing, and compare in constant time. Reject the request if\nthey differ.\n",
        "schema": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "examples": [
            "3f1c9a0d2b7e4f5a6c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
          ]
        }
      }
    }
  },
  "webhooks": {
    "stock_available_updated": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "stockAvailableUpdated",
        "summary": "Stock available quantity changed",
        "description": "Fires when a product variant's **available quantity** (พร้อมขาย)\nchanges — an order reserving stock, a manual adjustment, a return, a\ncancelled shipment, and so on. `data.items[].reason` says which.\n\nChanges are batched, so one request may carry several items. Items are\nnot guaranteed to arrive in order across requests; sequence them by\n`update_time`.\n\n`data.items[].id` is the same id the REST API calls\n`product_variant_id`, so it can go straight into\n`POST /v1/order/create`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/XXSellySignature"
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "description": "Always `xselly-webhook/1.0`.",
            "schema": {
              "type": "string",
              "examples": [
                "xselly-webhook/1.0"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StockAvailableUpdatedEvent"
              },
              "examples": {
                "orderAndAdjustment": {
                  "summary": "One order reservation and one manual adjustment",
                  "value": {
                    "request_id": "evt_018f4b3c2a7e7d3ab1c9d2e4f5a6b7c8",
                    "event_type": "stock_available_updated",
                    "request_time": 1718385160415,
                    "data": {
                      "items": [
                        {
                          "id": "456313132",
                          "sku": "SHIRT-RED-M",
                          "old": 12,
                          "new": 11,
                          "warehouse_id": "12345",
                          "update_time": 1718385160123,
                          "reason": "order_reserved",
                          "order_id": "178465431"
                        },
                        {
                          "id": "456313134",
                          "sku": "SHIRT-BLUE-L",
                          "old": 14,
                          "new": 50,
                          "warehouse_id": "12345",
                          "update_time": 1718385160123,
                          "reason": "user_adjusted",
                          "user_id": "45431"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged. Any 2xx status, returned within **1 second**. The body\nis ignored. Do heavy processing after responding, not before.\n"
          },
          "default": {
            "description": "Anything else — a non-2xx status or no answer within one second —\nmeans the delivery failed. It is **not** retried.\n"
          }
        },
        "security": []
      }
    }
  }
}