All SandPay errors follow a single, stable format. No surprises, no ad-hoc shape depending on the endpoint.
{
"error": "invalid_body",
"message": "amount must be greater than 0",
"detail": {
"fieldErrors": {
"amount": ["Number must be greater than 0"]
}
}
}
error — machine-readable code, stable — use it in your logic (switch, if).
message — human-readable text, in English, subject to change.
detail — optional structured payload (per-field Zod errors, additional context).
The X-Request-Id header is included on every response (success and error alike). Keep it if you open a support ticket.
Codes by HTTP status
| Status | error | When |
|---|
| 400 | invalid_body | The JSON body fails schema validation (Zod). detail contains the failing fields. |
| 400 | invalid_params | Invalid query parameters (e.g. limit=999). |
| 401 | unauthorized | Authorization header is missing. |
| 401 | invalid_api_key | Key is unknown, revoked, or malformed. |
| 402 | quota_exceeded | Monthly quota reached on the current plan. Upgrade your plan. |
| 404 | not_found | Resource not found (transaction, application, client…). |
| 404 | env_not_found | The country/operator pair has no configured environment. |
| 429 | rate_limited | Too many requests — retry after the Retry-After header. |
| 5xx | internal | Error on SandPay’s side. Tracked in Sentry, retry recommended. |
Retry strategy
| Status | Retriable? | Notes |
|---|
| 4xx (except 429) | No | Fix the request. Retrying will not help. |
| 429 | Yes | Honour Retry-After, then exponential backoff. |
| 5xx | Yes | Exponential backoff: 1s, 2s, 4s, 8s, 16s. Max 5 attempts. |
The Node SDK raises a SandPayApiError that exposes status, code, message, detail, and requestId — use it to wire up your retry logic.
Example — handling a quota exceeded error
import { SandPay, SandPayApiError } from "@sandpay/node";
const sp = new SandPay({ apiKey: process.env.SANDPAY_API_KEY! });
try {
await sp.payments.create({ /* ... */ });
} catch (err) {
if (err instanceof SandPayApiError && err.code === "quota_exceeded") {
// Notify the team, trigger an upgrade, etc.
notifyOps("SandPay quota exceeded, requestId=" + err.requestId);
return;
}
throw err;
}