Skip to main content
Version: 1.1.0

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:

CauseStatusBody
Authorization header absent401{"message":"Unauthorized"}
Header malformed, or api_key not recognised500{"message":"Internal Server Error"}
api_key recognised, secret wrong403{"message":"Forbidden"}
Rate limit exceeded403{"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_key across every endpoint, method and route.
  • Over the limit you get 403 — not 429. There is no Retry-After header and no machine-readable rate-limit body; a throttled request is byte-identical to a wrong-secret response.
  • A 403 is either a throttle or a wrong secret on a recognised api_key — the two are indistinguishable. Do not treat a burst of 403s 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 403 you 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 looping GET /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/r2s returns {"success":false,"message":"..."} on a rejected SKU — the only endpoint with a success field
  • 500 returns {"errorMessage":"Internal Server Error"}
  • 403 from 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:

  1. First request: omit cursor.
  2. Read nextCursor from the response and pass it as ?cursor={value}.
  3. nextCursor: null means 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:

EndpointCache-Control
GET /partner/lowest/{sku}public, max-age=300, s-maxage=600, stale-while-revalidate=120
GET /partner/settled/orderspublic, max-age=600, s-maxage=1200, stale-while-revalidate=180
GET /partner/live/listings/{type}, GET /partner/processing/orderspublic, 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/manifestprivate, 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/deltapublic, max-age=60, s-maxage=60
POST /partner/lowest/bulknot cached (POST)

Writes are not idempotent​

No write endpoint takes an idempotency key, and repeating one is not automatically safe:

  • PUT /partner/confirm/order performs 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 with GET /partner/{order_id} before deciding to resend.
  • PUT /partner/update/tracking writes 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/ask and POST /partner/list/preorder create 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​

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

Contact

SASOM Engineering:

URL: https://sasom.co.th