Requests, errors and limits
How every route answers, what errors look like and how to retry safely.
Base URL and version
Call https://api.intersink.com/v1. The version is in the path, and every response carries X-Kodkila-Version: 1. A version the server doesn’t serve answers 404 with UNKNOWN_API_VERSION. Each route also answers under the older /api prefix, which is what the SDK and the OpenAPI document’s path keys use; new code should call /v1.
Requests and responses are JSON over HTTPS. Uploads use multipart/form-data, and large media uses resumable uploads.
Successful answers
The payload is in data. Lists also carry meta for paging:
{
"success": true,
"statusCode": 200,
"message": "Success",
"data": {
"data": [],
"meta": { "total": 0, "page": 1, "limit": 2, "has_more": false }
},
"timestamp": "2026-11-01T12:46:49.315Z"
}Ask for the next page while has_more is true. Search has its own deeper paging with search_after.
Errors
{
"success": false,
"statusCode": 400,
"message": "limit must not be greater than 100",
"errorCode": "VALIDATION_FAILED",
"errors": ["limit must not be greater than 100"],
"path": "/v1/products?limit=200",
"requestId": "182d98bc-e4f2-4208-8c8e-aedd01aa2b17"
}Branch on errorCode, never on message. Show errors next to the fields they name. Keep requestId in your logs: support can find the request with it.
| Status | Usually means |
|---|---|
| 400 | The input failed validation (VALIDATION_FAILED), or a header such as an idempotency key is malformed |
| 401 | No session or it expired (UNAUTHENTICATED, TOKEN_INVALID): refresh once. A wrong code is INVALID_CODE and is not a refresh case. |
| 403 | Not allowed: a missing scope, a test key trying to write, or another channel |
| 404 | Not found, or not visible to you |
| 409 | A conflict, such as a version mismatch or the same request still running |
| 429 | Too many requests: wait as long as Retry-After says |
| 5xx | Our side. Retry later with backoff; the message is safe to show |
Rate limits
Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Requests are counted per key first, then per signed-in user, then per client IP; every visitor of an app shares the app key’s count. Sign-in code routes have tighter limits of their own.
On a 429, read Retry-After (also retry_after in the body), wait at least that long, then retry with exponential backoff and jitter. Cache what you read, debounce search boxes, and never call the API in a loop.
Retrying writes safely
Orders, checkout and payments accept an Idempotency-Key header. Create one key per user action, such as one click on Pay, and send the same key and body on every retry of that action:
const key = crypto.randomUUID(); // once, when the user clicks Pay
await api.post("/api/orders", {
body: { items: [{ product_id, quantity: 1 }], payment_method: "COD" },
idempotencyKey: key, // reuse it on every retry of this order
});| When you retry | You get |
|---|---|
| After the first request finished | The first answer again, with Idempotent-Replayed: true. Nothing runs twice. |
| While the first is still running | 409. Wait a second or two and retry with the same key. |
| With the same key but a different body | 422. Make a new key for a new action. |
| After the first one failed | The request runs again. Failures are not stored. |
Keys are 8 to 128 characters of letters, digits, _ and -, and are remembered for 24 hours.
Conditional writes
Per-user app data uses versions: send If-Match with the version you read. On a conflict, read again, merge and retry once.