Public API
Errors & rate limits
What a failed request looks like, which status and error codes to expect, and how many requests you can make.
Error format
A failed request returns a non-2xx status and the error envelope:
json
{
"success": false,
"error": {
"code": "LIMIT_EXCEEDED",
"message": "Limit exceeded for keys. Maximum allowed: 10000",
"statusCode": 403,
"details": { "resource": "keys", "limit": 10000 }
}
}| Field | Meaning |
|---|---|
code | Machine-readable reason. Stable — safe to branch on. |
message | Human-readable explanation. May change wording; show it, don't parse it. |
statusCode | Same as the HTTP status. |
details | Optional extra data: invalid fields for validation errors, resource and limit for plan limits. |
Status codes
| Status | Typical code | What to do |
|---|---|---|
400 | VALIDATION_ERROR, BAD_REQUEST | A field is missing or invalid. Fix the request — retrying won't help. |
401 | UNAUTHORIZED | The API key is missing, unknown, expired or revoked. |
403 | FORBIDDEN | The key lacks the scope, the endpoint is closed to API keys, or your role in a shared service doesn't allow it. |
403 | LIMIT_EXCEEDED | Your plan's limit for details.resource is reached. Upgrade or delete something first. |
404 | NOT_FOUND, KEY_NOT_FOUND, SERVICE_NOT_FOUND | The resource doesn't exist, or you can't see it. |
409 | CONFLICT, SLUG_ALREADY_EXISTS | Clashes with existing data, e.g. a taken key-page slug. |
422 | KEY_VALIDATION_ERROR, BATCH_COUNT_EXCEEDED | The request is well-formed but breaks a rule, e.g. hwid together with no_hwid. |
429 | TOO_MANY_REQUESTS | Rate limit hit. Wait for Retry-After seconds, then retry. |
5xx | INTERNAL_SERVER_ERROR | Our side. Retry with exponential backoff. |
Rate limits
Two limits apply to every request made with an API key, measured per minute:
| Limit | Applies to | Requests / minute |
|---|---|---|
| Per key | Each API key on its own | 120 |
| Per account | All your API keys together | Free 60 · Trial 120 · Premium 300 · Enterprise 900 |
Whichever runs out first returns 429. Each response carries X-RateLimit-Plan with the plan the account budget was computed for; a 429 from the account budget also carries Retry-After (seconds).
To stay under the limits:
- Batch. Create up to 100 keys in one request instead of 100.
- Cache. Services, providers and revenues rarely change — fetch them once, not per order.
- Spread load. On Premium and Enterprise the account budget is higher than one key's 120/minute, so give each integration its own key.
Retrying safely
| Retry? | When |
|---|---|
| Yes, with backoff | 429, 500, 502, 503, 504, network timeouts. |
| No | Other 4xx — the same request will fail the same way. |
Creating keys is not idempotent: if a POST /api/keys times out, the key may still have been created. Put your order ID in key_name and check List keys with search=<order id> before you retry, so a buyer never gets two keys.
async function santiauth(path, init = {}, attempt = 0) {
const res = await fetch(`https://api.santiauth.com${path}`, {
...init,
headers: {
"x-api-key": process.env.SANTIAUTH_API_KEY,
"Content-Type": "application/json",
...init.headers,
},
});
const retryable = res.status === 429 || res.status >= 500;
if (retryable && attempt < 4) {
const wait = Number(res.headers.get("Retry-After")) * 1000 || 500 * 2 ** attempt;
await new Promise((r) => setTimeout(r, wait));
return santiauth(path, init, attempt + 1);
}
const body = await res.json();
if (!body.success) throw Object.assign(new Error(body.error?.message), { status: res.status, code: body.error?.code });
return body.data;
}