Vouchereto API · v1
Idempotency, errors, limits
The rules every endpoint follows. Read this before you write retry logic.
Idempotency
Every POST needs an Idempotency-Key header. Use a value that identifies the operation on your side, such as order-3141-reserve, and send the same value when you retry after a timeout or a 5xx.
| You send | You get |
|---|---|
| A new key | The request runs normally. |
| The same key and the same body (a retry) | The original status and body, with the header Idempotent-Replayed: true. Nothing runs twice. |
| The same key with a different body | 409 IDEMPOTENCY_CONFLICT |
| The same key while the first request is still running | 409 IDEMPOTENCY_IN_PROGRESS: retry shortly |
| No key on a POST | 400 IDEMPOTENCY_KEY_REQUIRED |
Keys are scoped to your merchant and mode. Responses to keys are kept, so a retry days later still returns the first result; use a new key for a new operation.
Safe retry policy
- Retry on network errors, timeouts,
5xx,409 IDEMPOTENCY_IN_PROGRESSand429, with the same idempotency key, exponential backoff and jitter (for example 1 s, 2 s, 4 s, then give up and reconcile). - On
429, wait at leastRetry-Afterseconds. - Do not retry other
4xx: fix the request or tell the customer. - When unsure whether a capture went through,
GET /v1/redemptions/{id}tells you.
Errors
Errors use RFC 7807/9457 problem details. Branch on code (stable), show your own message to the customer, and log request_id: with it, support finds your exact call.
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://www.vouchereto.com/docs/errors/voucher_already_redeemed",
"title": "voucher already redeemed",
"status": 409,
"code": "VOUCHER_ALREADY_REDEEMED",
"request_id": "req_8f2c41d09a7b4e11"
}
Status families: 400 invalid request · 401 authentication · 403 not allowed in this mode or account state · 404 not found · 409 conflicting state · 422 valid request a business rule rejects · 429 slow down · 500 our side (retry with the same idempotency key). Every code, what it means and what to do.
Rate limits
| Scope | Limit |
|---|---|
POST /v1/redemptions | 120 requests / minute / key |
POST /v1/checkout-sessions | 300 requests / minute / key |
| Every other endpoint | 600 requests / minute / key |
| Failed authentications | 30 / minute / IP, then 60 s of 429 |
| Invalid voucher codes | 5 per customer (customer_ref) per 15 min, 20 per customer IP, 500 per merchant; blocks start at 5 minutes and double up to 24 hours (429 TOO_MANY_INVALID_ATTEMPTS) |
Need more throughput? Tell us your expected peak.
Pagination
List endpoints return data and pagination. Pass limit (1–100, default 20) and, for the next page, cursor = the previous next_cursor. Results are newest first and stable while you page.
GET /v1/redemptions?status=captured&limit=2
{
"data": [ { "id": "f4e8…", "object": "redemption", … }, { "id": "2b91…", … } ],
"pagination": { "has_more": true, "next_cursor": "eyJ0IjoiMjAyNi0xMC0yMlQxNzoyMjo1NS45MTVaIiwiaSI6IjJiOTEuLi4ifQ" }
}
GET /v1/redemptions?status=captured&limit=2&cursor=eyJ0Ijoi…
Formats
- Amounts are integers in the currency's minor unit:
5000USD = $50.00; JPY and KRW have no decimals, so1000JPY = ¥1,000. - Currencies are ISO 4217 upper-case codes; your account accepts a configured set (USD by default).
- Timestamps are ISO 8601 in UTC, for example
2026-10-22T17:22:56.118Z. - IDs are UUIDs. Every object has an
objectfield (redemption,checkout_session…) andlivemode. - customer_ref is your id for the customer (max 128 chars). Do not put emails or names in it.
- Request bodies are strict: unknown fields return
400 VALIDATION_ERROR. Responses may gain new fields at any time; ignore what you do not know. - Request bodies are limited to 64 KB.