# Errors | Vitix Pay

> How the Vitix Pay API reports errors: HTTP status codes, error types and codes, request ids, idempotency conflicts and rate limits.

Canonical: https://vitix.net/developers/errors
Markdown: https://vitix.net/developers/errors.md


Documentation menu

Vitix Pay uses conventional HTTP status codes and returns one error object per failed request. Branch on the stable code, show the human-readable message, and log the request id.

## The error object
400 Bad Request

```
{ "error": { "type": "invalid_request_error", "code": "amount_too_small", "message": "Amount is below the minimum of $10.00 (≈ 260,000 VND).", "param": "amount", "request_id": "req_dCjruuTgsXzq3EwlIAWS" }}
```

| Field | Description |
| --- | --- |
| type | The category: authentication_error, invalid_request_error, idempotency_error, rate_limit_error or api_error. |
| code | A stable machine-readable reason. New codes may be added; treat unknown codes as generic failures of their type. |
| message | A sentence for developers. Safe to log; not meant to be shown to end users verbatim. |
| param | The offending body or query parameter, when there is one (e.g. amount, success_url, id). |
| request_id | Unique id of this request (req_…), also sent as the request-id response header. Quote it when contacting support. |

## HTTP status codes

| Status | Meaning |
| --- | --- |
| 200 / 201 | Request succeeded (201 for newly created checkouts, refunds, endpoints and deliveries). |
| 400 | The request was malformed or violated a rule: missing parameter, wrong type, amount out of range, idempotency conflict. |
| 401 | No API key, a malformed key, or a key that was revoked. |
| 403 | Valid key, but the account is suspended. |
| 404 | The id does not exist in this account and mode. Live keys cannot see test objects. |
| 429 | Rate limit exceeded: more than 100 requests in 10 seconds on one key. |
| 5xx | Something went wrong on Vitix's side. Safe to retry with the same Idempotency-Key. |

## Error codes

| type | code | HTTP | Meaning |
| --- | --- | --- | --- |
| authentication_error | api_key_missing | 401 | No Authorization header. |
| authentication_error | api_key_invalid | 401 | Malformed, unknown or revoked key. |
| authentication_error | account_suspended | 403 | The partner account is suspended; contact support. |
| invalid_request_error | parameter_invalid | 400 | A body or query parameter failed validation. param names it. |
| invalid_request_error | invalid_json | 400 | The body is not valid JSON. |
| invalid_request_error | invalid_content_type | 400 | POST bodies must be sent with Content-Type: application/json. |
| invalid_request_error | amount_too_small | 400 | Below the minimum ($10.00 live, or your account minimum). |
| invalid_request_error | amount_too_large | 400 | Above your account's per-checkout maximum. |
| invalid_request_error | url_not_allowed | 400 | success_url / cancel_url is not on an allowed domain, or is not https. |
| invalid_request_error | checkout_not_open | 400 | Expire called on a checkout that is already complete or expired. |
| invalid_request_error | amount_invalid | 400 | Refund amount exceeds the unrefunded balance, or the payment is fully refunded. |
| invalid_request_error | refund_pending | 400 | Another refund of the same payment is still processing. |
| invalid_request_error | refund_unavailable | 400 | The payment cannot be refunded through the API. |
| invalid_request_error | invoice_unavailable | 400 | Test payment, or the PDF is still being generated (retry shortly). |
| invalid_request_error | limit_reached | 400 | More than 10 webhook endpoints in one mode. |
| invalid_request_error | idempotency_key_invalid | 400 | Idempotency-Key longer than 255 characters. |
| invalid_request_error | resource_missing | 404 | No object with that id in this account and mode. |
| idempotency_error | idempotency_key_reused | 400 | The key was already used with a different path or payload. |
| rate_limit_error | rate_limit_exceeded | 429 | Slow down and retry with backoff. |
| api_error | api_error | 500 | Unexpected failure on Vitix's side. Retry; include request_id if you contact support. |

## Handling errors
With the [SDK](https://vitix.net/developers/sdk#errors), errors are thrown as typed classes. With raw HTTP, read `res.ok` and the `error` object:
ts

```
const res = await fetch("https://api.vitix.net/v1/checkouts", { method: "POST", headers, body });const json = await res.json();if (!res.ok) { const { type, code, message, param, request_id } = json.error; if (code === "amount_too_small" || code === "amount_too_large") return showFieldError("amount", message); if (type === "rate_limit_error") return retryLater(); if (res.status >= 500) return retryWithSameIdempotencyKey(); throw new Error(`Vitix Pay ${code} (${request_id}): ${message}`);}
```

## Idempotency conflicts
A replayed request (same key, same path, same body) returns the original response with `idempotent-replayed: true` and is never an error, even if the original was a 400. Reusing a key for a different request is rejected with `idempotency_key_reused` so a bug in your key generation cannot silently return the wrong checkout. Keys live for 24 hours.

## Rate limits
Each key may make 100 requests per 10 seconds. A `429` response carries the standard error body; wait at least a second, add jitter, and retry. If you need sustained higher throughput, batch reconciliation reads with `limit=100` and avoid polling: webhooks push every change to you.

[← Webhooks](https://vitix.net/developers/webhooks)[API reference →](https://vitix.net/developers/api)
Questions or a missing feature? Email [support@vitix.net](mailto:support@vitix.net)
