Errors
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
{ "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, errors are thrown as typed classes. With raw HTTP, read res.ok and the error object:
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.