Errors
HTTP status codes and error codes returned by the Teyra Pay API, and how to handle each one.
Teyra Pay uses standard HTTP status codes. 2xx means success, 4xx means the request has a problem you can fix, and 5xx means something went wrong on our side or with the payment network.
Every error has the same JSON shape:
{
"error": {
"code": "validation_error",
"message": "Some fields are missing or invalid.",
"details": {
"fields": {
"amount": "Expected integer, received float",
"currency": "Invalid enum value. Expected 'KWD' | 'BHD' | 'OMR' | 'JOD' | 'SAR' | 'AED' | 'QAR' | 'EGP', received 'USD'"
}
}
}
}| Field | Description |
|---|---|
code | Stable, machine-readable code. Branch on this. |
message | Human-readable explanation, for your logs. It may change; don't parse it. |
details | Optional extra data. For validation_error, details.fields maps each invalid field to a message. Nested fields use a dotted path, such as customer.email or metadata.order_id. |
Error codes
| HTTP | code | Meaning | What to do |
|---|---|---|---|
| 401 | unauthorized | Missing, malformed, revoked or unknown API key. | Check the Authorization: Bearer sk_… header and the key's mode. |
| 403 | forbidden | The key is valid but not allowed to do this. | Check you're using the right key and account. |
| 403 | merchant_not_active | Live request before the account is approved and activated, or the account is suspended. | Use a test key, or complete onboarding. |
| 404 | payment_not_found | The payment doesn't exist, or it belongs to the other mode (test/live). | Check the ID and the key's mode. |
| 404 | refund_not_found | The refund doesn't exist, or it belongs to the other mode. | Check the ID and the key's mode. |
| 404 | payment_link_not_found | The payment link doesn't exist, or it belongs to the other mode. | Check the ID and the key's mode. |
| 404 | not_found | Any other object or path that doesn't exist, such as a webhook endpoint. | Check the ID, the path and the key's mode. |
| 409 | conflict | A request with the same Idempotency-Key is still being processed. | Wait a moment and retry with the same key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with a different request. | Use a new key for a new request. See Idempotency. |
| 422 | validation_error | One or more parameters are missing or invalid, including a live-mode URL that isn't https://, a payment_method your account doesn't support ("unsupported"), or, on some accounts, a three-decimal amount that doesn't end in 0. | Read details.fields, fix the request. See Payment methods and your account. |
| 422 | refund_exceeds_amount | The refund is more than what remains refundable. details.remaining holds what is left. | Refund at most amount − amount_refunded. |
| 422 | payment_not_refundable | The payment isn't succeeded, so it can't be refunded. | Only refund succeeded payments. |
| 429 | rate_limited | Too many requests with this key (1,200 per minute). details.retry_after_seconds and the Retry-After header say when to retry. | Wait, then retry with the same Idempotency-Key. See Rate limits. |
| 500 | internal_error | Unexpected error on our side. | Retry with the same Idempotency-Key. Contact support if it persists. |
| 502 | gateway_error | The payment network returned an error or didn't respond. | Retry with the same Idempotency-Key after a short delay. |
New error codes may be added. Treat an unknown code according to its HTTP status.
Handling errors
const res = await fetch('https://api.teyra.co/v1/payments', options);
const body = await res.json();
if (!res.ok) {
const { code, message, details } = body.error;
if (code === 'validation_error') console.warn('Invalid fields', details?.fields);
else if (res.status === 429 || res.status >= 500) await retryLater(); // keep the same Idempotency-Key
throw new Error(`Teyra Pay ${res.status} ${code}: ${message}`);
}Payment failures are not API errors
A declined card does not produce an HTTP error. Creating the payment succeeds (201), and the payment later moves to status: "failed" with failure_code and failure_message set. You learn about it from GET /v1/payments/:id or the payment.failed webhook.