Conventions
Every endpoint follows the same conventions for status codes, errors, retries, and pagination. Learn them once and they apply everywhere.
| Status | Meaning |
|---|---|
200 | Success. |
201 | A resource was created (a guest, a hold, or a 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. |
503 | Maintenance. The platform is temporarily unavailable. Retry after the Retry-After header. |
Every failure returns the matching status and a single error object:
{
"error": {
"type": "permission",
"code": "insufficient_scope",
"message": "Missing required scope: reservations:write.",
"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: the request field that caused the error. Present only on400field-validation errors, and omitted otherwise.estimated_end: an ISO timestamp for when a maintenance window is expected to end. Present only on503, and only when an end time has been set. It is more precise than theRetry-Afterheader, which is derived from it, so prefer it when rendering a "back at" state.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. |
idempotency_in_progress | conflict | An earlier request with the same Idempotency-Key is still running. Retry shortly. |
insufficient_points | conflict | A loyalty redemption exceeds the available balance. |
rate_limited | rate_limit | The per-key request limit was exceeded. |
maintenance_mode | server | The platform is in a maintenance window. Retry after the Retry-After header. |
All Veridien resource ids are UUIDs, for example 7d5dc2ea-1f4e-4bfa-9a52-6c3f08b41e27. The guest-facing booking reference is separate: confirmation_code is a short human-readable code (for example B7XKQ4M2NZ) shown to guests and staff, never used as a resource id. Request ids are prefixed with req_. Fields you supply, such as payment_reference, are opaque to Veridien and keep whatever format your system uses.
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": "9b2f6c3e-8d1a-4f5b-a7c9-4e0d2b8f1a63", "rate_plan_id": "5c1e8f4a-2b7d-4a9c-8e6f-1d3b9a5c7e02", "check_in": "2026-08-01", "check_out": "2026-08-04", "guest_id": "3f8a2c1b-9d4e-4c6a-8b7f-5e2d1a9c0b34", "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. - Sending the same key concurrently, before the first request has finished, returns
409 idempotency_in_progress. Retry after a moment and you will get the stored response. The key is claimed before the operation runs, so the second request never executes it. - A request that fails releases its key, so an honest retry runs again rather than replaying the failure. Only successful responses are stored: a
409 no_availabilityfor a momentary sell-out is not replayed for ever once the room frees up.
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.
GET /reservations pages with limit and offset. Other list endpoints (/room-types, /services, /rate-plans) return the full set for the property and do not page.
| Parameter | Default | Max | Meaning |
|---|---|---|---|
limit | 25 | 100 | How many records to return. |
offset | 0 | 10000 | 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.
Every monetary value is a decimal string, never a float: "1310.40", not 1310.4. Parse them with a decimal type, not a binary floating-point one.
A property has one base currency, and a folio can carry rows in several currencies at once (a USD room bill with an MVR restaurant line is normal). Two rules follow from that:
Amounts in different currencies are never added together. Anywhere a balance is reported, a balances array gives the per-currency actuals, and that is the figure to trust:
"balances": [
{ "currency": "USD", "charges": "1310.40", "payments": "0.00", "balance": "1310.40" },
{ "currency": "MVR", "charges": "1500.00", "payments": "1500.00", "balance": "0.00" }
]A folio is settled only when nothing is owed in any currency. A USD debt is never cancelled out by an MVR credit.
Flat totals are a valuation, and can be null. Fields like balance, total_charges and folio_balance are convenience figures valued in the property's base currency, built from the exchange rate frozen onto each row at the moment it was written. Because the rate is frozen, a later rate change never alters the reported value of an existing charge. If a row is in a currency the property has no configured rate for, it cannot be valued, and these fields are null rather than a wrong number. Handle null before doing arithmetic.
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 at least the indicated number of seconds, then retry. Prefer caching reads (/property, /room-types and /services carry a short Cache-Control; /availability and /rates are live inventory and intentionally uncached, so do not cache them yourself) and avoid tight polling loops.
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.