Skip to main content

Error Handling

There is no response envelope​

This API does not wrap responses. The body is the object the endpoint documents, at the top level.

// GET /partner/processing/orders — 200
{
"type": "processing",
"orders": [ /* ... */ ],
"nextCursor": "eyJ0Ijp7..."
}
Do not branch on a success field

There is no success field on a successful response, and no data wrapper. Code like if (!body.success) throw ... treats every successful response as a failure. Branch on the HTTP status code.

The one exception: POST /partner/batch/r2s returns {"success": false, "message": "..."} when it rejects a SKU. It is the only endpoint in the API with a success field, and it only appears on that error path — never on success.

HTTP status codes​

StatusMeaningNotes
200OK
201CreatedListing creates. Also returned when nothing was created — see vacation mode below.
400Bad requestValidation failure, wrong order status, order not found, and order-not-yours
401UnauthorizedAuthorization header absent
403ForbiddenWrong secret on a recognised api_key, or rate limited — the two are identical
404Not foundRoute does not exist. Not used for missing orders.
500Internal server errorA genuine server fault — but also an unrecognised api_key or a malformed auth header
503Service unavailableCatalog endpoints only, when no snapshot is servable

Codes this API never returns: 429, 409, 422. Do not write handlers for them.

Error body shapes​

There are four, and which one you get depends on the endpoint.

Most 400s​

{ "statusCode": 400, "message": "status not allowed status: matched" }

Note the statusCode is repeated inside the body. It matches the HTTP status.

Pricing and catalog endpoints​

{ "message": "SKU not found", "code": "0009" }

These carry a stable machine-readable code. Branch on code, not on message text.

codeMeaning
0000Success
0009SKU not found (400)
0010since snapshot expired or chain broken — fetch a fresh manifest (400)
0011since missing or empty (400)
0012No catalog snapshot has been built yet (503)
0013Next snapshot's change set too large — fetch a fresh manifest (400)
0014Catalog snapshot is stale; use the price endpoints (503)

500​

{ "errorMessage": "Internal Server Error" }

Different key — errorMessage, not message. Handle both.

403 from the authorizer​

{ "message": "Forbidden" }

The traps worth knowing about​

A 500 can mean "bad credential"​

The auth path does not fail with one status. An unrecognised api_key, or a malformed Authorization header, makes our authorizer fail rather than deny, and you receive:

{ "message": "Internal Server Error" }

at HTTP 500. Check your credential before reporting an outage — this is the normal response to a wrong or not-yet-provisioned key. It is a known defect on our side and will become a 403 when fixed, so do not depend on 500 meaning "bad key".

A missing Authorization header returns 401 {"message":"Unauthorized"}.

403 is ambiguous​

A wrong secret on a recognised api_key and an exceeded rate limit both return 403 {"message":"Forbidden"}. Check your request rate before concluding your credentials are broken. See Rate Limiting.

A different 403, {"statusCode":403,"message":"Supplier account not configured"}, means the credential authenticated but the supplier account is incomplete — that one is a provisioning issue for your account manager.

400, not 404, for someone else's order​

Passing an order ID that belongs to another supplier returns:

{ "statusCode": 400, "message": "uid not match" }

A non-existent order ID returns 400 with "not Found order ID: {id}". Neither is a 404. 404 from this API means the route does not exist — check your path and method.

GET /partner/label/{orderId} and PUT /partner/update/tracking are the exceptions: they return {"statusCode":403,"message":"Unauthorized: order ... does not belong to this supplier"} in the body, at HTTP 400. So the body's statusCode and the HTTP status can disagree on those two. Trust the body's message; do not rely on the HTTP status to distinguish an ownership failure.

201 does not always mean created​

POST /partner/list/ask returns 201 when your account is in vacation mode, with no listing created and no order_id:

{ "status": "user is in vacation mode" }

Branch on the presence of order_id, not on the status code:

const created = await response.json();
if (!created.order_id) {
// Vacation mode, or otherwise not listed. Nothing was created.
throw new Error(`Listing not created: ${created.status ?? 'unknown reason'}`);
}

Out-of-range prices are clamped, not rejected​

There is no "price too low" error. A price below the floor is silently stored at the floor and you get a normal 201/200:

EndpointFloorBehaviour below it
POST /partner/list/ask50 THBClamped to 50
POST /partner/list/preorder50 THBClamped to 50
PUT /partner/edit/listing100 THBClamped to 100
PUT /partner/edit/preorder100 THBClamped to 100

Validate prices yourself before sending — the API will not tell you a price was adjusted, and the listing goes live at the clamped value.

POST /partner/batch/r2s does not validate much​

price, quantity and size are stored as sent. A negative price or an implausible quantity is accepted and becomes a warehouse intake record someone has to fix by hand. Validate before sending; see the endpoint reference.

200 does not mean every item succeeded​

Two endpoints apply partially:

  • PUT /partner/update/tracking writes orders one at a time. Order IDs that do not exist are skipped silently and still return 200. If an ID belongs to another supplier, the call stops there — orders earlier in the array are already written, with no rollback.
  • POST /partner/lowest/bulk returns 200 with results, not_found, deferred and errors arrays. A 200 means the request was processed, not that every SKU resolved. Check all four arrays.

Reconcile against the read endpoints rather than trusting a 200.

A handler that matches reality​

class SasomApiError extends Error {
constructor(status, body) {
super(
body?.message ??
body?.errorMessage ??
`SASOM Partners API returned ${status}`,
);
this.name = 'SasomApiError';
this.status = status;
this.code = body?.code ?? null;
this.body = body;
}
}

async function parseBody(response) {
try {
return await response.json();
} catch {
return null;
}
}

async function sasomRequest(path, options = {}) {
const response = await fetch(`https://partners.sasomapi.com${path}`, {
...options,
headers: {
Authorization: authorization,
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip',
...options.headers,
},
});

const body = await parseBody(response);

// 2xx: the body IS the payload. No envelope, no `success` field to check.
if (response.ok) return body;

// 403 is ambiguous: throttle, wrong secret, or unconfigured account.
// The distinguishable case is the one with a body message.
if (response.status === 403) {
throw new SasomApiError(403, body ?? { message: 'Forbidden' });
}

// A bare 500 on the auth path is very often an unrecognised api_key rather
// than a server fault, so say so instead of retrying it as a transient error.
if (response.status === 500 && body?.message === 'Internal Server Error') {
throw new SasomApiError(500, {
message:
'Internal Server Error — if this is reproducible, verify SASOM_API_KEY: ' +
'an unrecognised api_key currently returns 500 rather than 403',
});
}

throw new SasomApiError(response.status, body);
}

Reporting a problem​

Submit a ticket with the endpoint and method, the request body with prices and IDs intact, the HTTP status, the full response body, and the time in UTC.

Never include your api_key or secret in a ticket, an email, or a screenshot. Identify yourself by business name — SASOM support can look up your account without the credential.