SASOM Partners API
API for SASOM partner sellers to manage product listings, orders, and logistics.
Authentication
Every endpoint requires HTTP Basic Authentication. There is no unauthenticated route and no public route.
Authorization: Basic base64(api_key:secret)
Credentials are issued per supplier by SASOM. Contact your SASOM account manager
to obtain your api_key and secret.
This is a server-to-server API. Your secret is a bearer credential for
your entire seller account — it can create listings, reprice them, confirm
orders and download buyer shipping labels. Never place it in a browser, a
mobile app, or any client you do not control, and never call these endpoints
from front-end code.
Do not rely on the browser to stop you. These endpoints return permissive
CORS headers (Access-Control-Allow-Origin: *), so a cross-origin call from
page JavaScript is not reliably blocked. The credential is compromised the
moment it reaches a client, whether the request succeeds or not.
Authentication failures do not all return the same status. There are three
distinct outcomes, and one of them is a 500:
| Cause | Status | Body |
|---|---|---|
Authorization header absent | 401 | {"message":"Unauthorized"} |
Header malformed, or api_key not recognised | 500 | {"message":"Internal Server Error"} |
api_key recognised, secret wrong | 403 | {"message":"Forbidden"} |
| Rate limit exceeded | 403 | {"message":"Forbidden"} |
A 500 on the auth path usually means your credential is wrong, not that
SASOM is down. An unrecognised api_key makes the authorizer fail rather
than deny, and the failure surfaces as 500. So during onboarding, or after a
typo in a key, expect 500 — check the credential before escalating. This is
a known defect on our side; the status will become a 403 when it is fixed, so
treat 401, 403 and 500 on the auth path as the same class of problem:
verify the credential and your request rate, and only escalate if a
known-good credential keeps failing.
Credential changes are not instant. A newly issued credential works immediately, but a rotated or revoked one can keep authenticating for up to 5 minutes while the previous secret ages out of cache. Treat rotation as "the old secret is live for another 5 minutes", and tell SASOM immediately if a secret is exposed — revocation starts that clock, it does not stop traffic at once.
Rate limiting
- 200 requests per 60 seconds, counted against your
api_keyacross every endpoint, method and route. - Over the limit you get
403— not429. There is noRetry-Afterheader and no machine-readable rate-limit body; a throttled request is byte-identical to a wrong-secret response. - A
403is either a throttle or a wrongsecreton a recognisedapi_key— the two are indistinguishable. Do not treat a burst of403s as "our credentials broke" if they were working a minute ago; check your request rate first. - The window is a rolling 60 seconds. Back off for a full 60 seconds after a
403you believe is a throttle, then resume at a lower rate. Do not retry tightly: retries consume the same budget and extend the throttle. - Design to stay under the limit rather than to recover from it: use
POST /partner/lowest/bulk(100 SKUs per request) instead of loopingGET /partner/lowest/{sku}, and the catalog manifest + delta loop instead of crawling SKUs.
Response format
Responses are the JSON object each endpoint documents, at the top level. There
is no envelope — no success field, no data wrapper. Branch on the HTTP
status code, and on the code field where an endpoint documents one.
Errors are not uniform, so read the per-endpoint responses:
- most
400s return{"statusCode":400,"message":"..."} - the pricing and catalog endpoints return
{"message":"...","code":"00xx"} POST /partner/batch/r2sreturns{"success":false,"message":"..."}on a rejected SKU — the only endpoint with asuccessfield500returns{"errorMessage":"Internal Server Error"}403from the authorizer returns{"message":"Forbidden"}
Send Accept-Encoding: gzip and successful responses come back gzipped; the
list endpoints are large enough that this matters.
Pagination
GET /partner/processing/orders, GET /partner/settled/orders and
GET /partner/live/listings/{type} are cursor-paginated:
- First request: omit
cursor. - Read
nextCursorfrom the response and pass it as?cursor={value}. nextCursor: nullmeans there are no more pages.
Page size is fixed by the server — 100 orders for the order lists, 500 for live
listings. There is no limit parameter; sending one has no effect. Treat
cursor as an opaque token: it is a base64 keyset token, its contents are not
a stable interface, and a cursor from one endpoint is not valid on another.
Caching
GET responses carry Cache-Control. Honour it rather than inventing your own
interval — these are the real values:
| Endpoint | Cache-Control |
|---|---|
GET /partner/lowest/{sku} | public, max-age=300, s-maxage=600, stale-while-revalidate=120 |
GET /partner/settled/orders | public, max-age=600, s-maxage=1200, stale-while-revalidate=180 |
GET /partner/live/listings/{type}, GET /partner/processing/orders | public, max-age=60, s-maxage=120, stale-while-revalidate=30 |
GET /partner/{order_id}, GET /partner/payout/{order_id} | public, max-age=30, s-maxage=60, stale-while-revalidate=15 |
GET /partner/catalog/manifest | private, max-age=300 — private on purpose: the body contains a presigned URL, which is itself a credential. Do not store this response in a shared or proxy cache. |
GET /partner/catalog/delta | public, max-age=60, s-maxage=60 |
POST /partner/lowest/bulk | not cached (POST) |
Writes are not idempotent
No write endpoint takes an idempotency key, and repeating one is not automatically safe:
PUT /partner/confirm/orderperforms a payout adjustment. The status change it triggers is asynchronous, so a retry sent within that window passes the status gate a second time and can deduct the seller shipping fee twice. Send it once; on a timeout, re-read the order withGET /partner/{order_id}before deciding to resend.PUT /partner/update/trackingwrites each order in the array in sequence and stops at the first order that is not yours — orders before it are already written. Send only your own order IDs, and re-read to confirm.POST /partner/list/askandPOST /partner/list/preordercreate a new listing per call. A retry creates a duplicate listing, not the same one.
Handling buyer data
GET /partner/label/{orderId} returns a presigned URL to a PDF containing the
buyer's full name, address and phone number. The URL grants access to that
PDF to anyone holding it, for up to 1 hour.
- Do not log the URL, put it in a ticket, or pass it to a third party.
- Download the PDF, use it for the shipment, and do not retain buyer personal data beyond what your own fulfilment and legal obligations require.
Prices are in Thai baht
Every price, payout and fee in this API is a THB amount, not satang.
5500 means ฿5,500.
Order lifecycle
pending → process → matched → settled
↓ ↓
failed failed
Pre-orders follow: pending → pre_order → process → matched → settled
A partner cannot move an order to failed. Cancellation and refund are handled
by SASOM operations — contact your account manager.
Authentication
- HTTP: Basic Auth
Use your supplier api_key as the username and secret as the password.
Authorization: Basic base64(api_key:secret)
Security Scheme Type: | http |
|---|---|
HTTP Authorization Scheme: | basic |