Errors
Every failed response follows the same JSON envelope. The code field is stable and machine-readable; the message is human-friendly (don't match on it).
Error envelope
{
"error": {
"code": "invalid_request",
"message": "amount must be a positive number",
"issues": {
"formErrors": [],
"fieldErrors": { "amount": ["Required"] }
}
}
}Common codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Body failed validation — see issues for field-level details. |
| 400 | invalid_state | Operation not allowed in the resource's current state (e.g. refunding a non-succeeded payment). |
| 400 | no_route | No enabled provider supports this method/currency combination. |
| 400 | insufficient_balance | Payout amount exceeds available balance. |
| 401 | unauthenticated | Missing or invalid API key, or key type mismatch (test key on live payment). |
| 403 | insufficient_scope | API key scopes don't include read or write as needed. |
| 404 | not_found | Resource doesn't exist or belongs to another merchant / environment. |
| 409 | email_taken | Signup collision — account already exists. |
| 409 | already_used | Email verification / activation token already consumed. |
| 410 | invalid_token | Token expired or invalid (password reset, email verify). |
| 429 | rate_limited | Per-key rate limit exceeded. Check Retry-After header for wait time. |
| 500 | internal_error | Unexpected server error. Safe to retry with backoff. |
| 502/504 | provider_error | Upstream provider (DirectPay, PayGram, etc.) failed. Safe to retry. |
Retrying
Safe to retry: 429, 500, 502, 504, and network errors (timeouts, ECONNRESET). Always include an Idempotency-Key header so duplicates don't create extra resources.
Do not retry on 400, 401, 403, 404, 409 — fix the request and call once.
Handling pattern
Node.js error handling
async function createPayment(body) {
const res = await fetch('https://magiapay.innoserver.cloud/v1/payments', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MAGIAPAY_SECRET}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!res.ok) {
const { error } = await res.json();
switch (error.code) {
case 'invalid_request': throw new UserInputError(error.message, error.issues);
case 'unauthenticated': await rotateCreds(); throw new RetryableError();
case 'rate_limited': await sleep(Number(res.headers.get('Retry-After') || '1') * 1000);
throw new RetryableError();
case 'provider_error': throw new RetryableError();
default: throw new Error(`${error.code}: ${error.message}`);
}
}
return res.json();
}