Conventions
These rules for status codes, errors, retries, and pagination apply to every endpoint. Learn them once and they hold everywhere.
Status codes
| Status | Meaning |
|---|---|
200 | Success. |
201 | A resource was created (a guest, a hold, a charge, a rate plan, an interval, or a promo code). |
304 | GET /openapi.json only: your cached copy is current. |
400 | Invalid request: malformed JSON, a body over 1 MiB, or a parameter that failed validation. |
401 | Authentication failed: missing or invalid API key. |
403 | The key is valid but lacks the required scope, or the action is not allowed. |
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, for example. |
429 | Rate limited. Retry after the Retry-After header. |
500 | Internal server error. |
503 | Maintenance. The platform is briefly unavailable. Retry after the Retry-After header. |
The error envelope
Every failure returns the matching status and one error object:
{
"error": {
"type": "permission",
"code": "insufficient_scope",
"message": "Missing required scope: reservations:write.",
"request_id": "req_0b9d3e6a-2f4c-4a1e-9c7b-5d8f1a3e6b20"
}
}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 readable explanation. Safe to log; do not parse it.param: the request field that caused the error. Present only on some400validation errors.estimated_end: an ISO timestamp for when a maintenance window should end. Present only on503, and only when an end time has been set. TheRetry-Afterheader is derived from it, so prefer it when you show a "back at" time.request_id: identifies the request. Include it when you contact 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:
| Code | Type | When |
|---|---|---|
missing_api_key / invalid_api_key | authentication | The Authorization header is missing, or the key is unknown, revoked, or 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. |
payload_too_large | invalid_request | The request body is over 1 MiB. |
unknown_route | not_found | No endpoint matches the path. |
not_found | not_found | An id is not a valid UUID, or no record matches it. |
method_not_allowed | invalid_request | The path exists, but not for this method (status 405). |
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 is more than the available balance. |
rate_limited | rate_limit | The per-key or per-IP request limit was exceeded. |
too_many_requests | rate_limit | Too many promo-code validation attempts. |
maintenance_mode | server | The platform is in a maintenance window. Retry after the Retry-After header. |
Each resource page lists the further codes its endpoints return.
Identifiers
Every Veridien resource id is a UUID, for example 7d5dc2ea-1f4e-4bfa-9a52-6c3f08b41e27. The confirmation code is separate: confirmation_code is a short readable code shown to guests and staff, and is never used as a resource id. Request ids are req_ followed by a UUID. Fields you supply, such as payment_reference, are opaque to Veridien and keep whatever format your system uses.
Idempotency
POST, PATCH, and DELETE requests change data: creating a guest, holding inventory, confirming a reservation, redeeming points, posting a charge, editing a rate plan. 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 YOUR_API_KEY" \
-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 get the stored response. The key is claimed before the operation runs, so the second request never runs 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 brief sell-out is not replayed once the room frees up.
Keys are scoped per API key. Use a fresh Idempotency-Key for each logical operation. POST /promo-codes/validate changes nothing, so it ignores the header.
Confirm is doubly idempotent
Confirming a hold (POST /holds/{id}/confirm) is also idempotent on its payment_reference: re-confirming a confirmed reservation, or replaying the same payment reference, returns the existing result with "already_confirmed": true rather than recording the payment again.
Pagination
GET /reservations pages with limit and offset. The other list endpoints 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.
Money and currency
Every amount is a decimal string, never a float: "1310.40", not 1310.4. Parse amounts 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. Wherever 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 when 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 rate for, it cannot be valued, and these fields are null rather than a wrong number. Handle null before you do arithmetic.
Rate limits
Rate limits use a token bucket per API key. Your bucket holds up to 600 tokens and refills at 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.
Each IP address also has its own, larger bucket (3,600 tokens, refilling at 60 per second), so several keys used from one server are not throttled together. Promo-code validation has a tighter limit of its own; see Offers and promo codes.
Authenticated responses carry RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, and RateLimit-Policy headers. When the bucket is empty, requests return 429 with a Retry-After header. Wait at least that many seconds, then retry.
Cache what you can and avoid tight polling loops. /property, /room-types, and /services send Cache-Control: private, max-age=60. /availability and /rates are live inventory and are not cached, so do not cache them yourself.
Request IDs
Every response, success or error, carries an X-Request-Id header. The same id appears as request_id in error bodies and in /me. Log it: when you report an issue, the request id lets support find the exact call.