Conventions
Every endpoint follows the same conventions for status codes, errors, retries, and pagination. Learn them once and they apply everywhere.
Status codes
| Status | Meaning |
|---|---|
200 | Success. |
201 | A resource was created (a guest, hold, reservation confirmation, or charge). |
400 | Invalid request — malformed JSON or a parameter that failed validation. |
401 | Authentication failed — missing or invalid API key. |
403 | The key is valid but lacks the required scope. |
404 | The route or resource does not exist (or is not yours). |
405 | The path exists but not for this HTTP method. |
409 | Conflict — no availability, an idempotency-key reuse, or an insufficient loyalty balance. |
429 | Rate limited. Retry after the Retry-After header. |
500 | Internal server error. |
The error envelope
Every failure returns the matching status and a single error object:
{
"error": {
"type": "permission",
"code": "insufficient_scope",
"message": "Missing required scope: reservations:write.",
"param": null,
"request_id": "req_a1b2c3d4e5"
}
}type— the category:invalid_request,authentication,permission,not_found,conflict,rate_limit, orserver.code— a stable, machine-readable identifier. Switch on this, not onmessage.message— a human-readable explanation. Safe to log; do not parse.param— when present, the request field that caused the error (for400s).request_id— uniquely identifies the request. Include it when contacting support.
Code over message
message text may change; code will not. Branch your error handling on error.code (for example, no_availability, rate_plan_not_found, insufficient_points).
Common codes you will encounter:
| Code | Type | When |
|---|---|---|
missing_api_key / invalid_api_key | authentication | The Authorization header is absent or the key is unknown/disabled/expired. |
insufficient_scope | permission | The key lacks the endpoint's scope. |
invalid_parameters / invalid_json | invalid_request | A field failed validation, or the body was not valid JSON. |
unknown_route | not_found | No endpoint matches the path. |
no_availability | conflict | The room type is sold out for the requested dates. |
idempotency_key_reuse | conflict | An Idempotency-Key was reused with a different request body. |
insufficient_points | conflict | A loyalty redemption exceeds the available balance. |
rate_limited | rate_limit | The per-key request limit was exceeded. |
Idempotency
POST and PATCH requests are the ones that change state — creating a guest, holding inventory, confirming a reservation, redeeming points, posting a charge. To make retries safe, send an Idempotency-Key header with a unique value (a UUID works well):
curl -X POST https://veridien.app/api/v1/holds \
-H "Authorization: Bearer vrdn_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1d2c9a-8b3e-4a17-9c2f-1e5b7d0a4c83" \
-d '{ "room_type_id": "rt_deluxe", "rate_plan_id": "rp_flex", "check_in": "2026-08-01", "check_out": "2026-08-04", "guest_id": "G7K2P9QX", "adults": 2 }'- The first request with a given key runs normally and its response is stored.
- A retry with the same key and the same body replays the stored response — no second hold, no second charge.
- Reusing the key with a different body returns
409 idempotency_key_reuse.
Keys are scoped per API key. Use a fresh Idempotency-Key per logical operation.
Confirm is doubly idempotent
Confirming a hold (POST /holds/{id}/confirm) is also idempotent on its payment_reference: re-confirming an already-confirmed reservation, or replaying the same payment reference, returns the existing confirmed result with "already_confirmed": true rather than charging again.
Pagination
List endpoints (such as GET /reservations) page with limit and offset:
| Parameter | Default | Max | Meaning |
|---|---|---|---|
limit | 25 | 100 | How many records to return. |
offset | 0 | — | How many records to skip. |
The response wraps results in data and reports whether more remain:
{
"has_more": true,
"data": [ /* ... */ ]
}To fetch the next page, add limit to your previous offset. Stop when has_more is false.
Rate limits
Rate limits use a leaky bucket (token bucket), per API key. Your bucket holds up to 600 tokens and refills at a constant 10 tokens per second (600 per minute). Every request spends one token:
- A burst can spend the whole bucket at once — up to 600 requests back to back.
- Sustained throughput is the refill rate, 10 requests per second.
When the bucket is empty, requests return 429 with a Retry-After header. Back off for the indicated number of seconds, then retry. Prefer caching reads (availability and rates carry a short Cache-Control) and avoid tight polling loops.
Request IDs
Every response — success or error — is associated with a request_id (also returned in error bodies and from /me). Log it. When you report an issue, the request id lets support trace the exact call.