Skip to content
Log in
Conventions

Conventions

These rules for status codes, errors, retries, and pagination apply to every endpoint. Learn them once and they hold everywhere.

Status codes

StatusMeaning
200Success.
201A resource was created (a guest, a hold, a charge, a rate plan, an interval, or a promo code).
304GET /openapi.json only: your cached copy is current.
400Invalid request: malformed JSON, a body over 1 MiB, or a parameter that failed validation.
401Authentication failed: missing or invalid API key.
403The key is valid but lacks the required scope, or the action is not allowed.
404The route or resource does not exist (or is not yours).
405The path exists, but not for this HTTP method.
409Conflict: no availability, an idempotency-key reuse, or an insufficient loyalty balance, for example.
429Rate limited. Retry after the Retry-After header.
500Internal server error.
503Maintenance. 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, or server.
  • code: a stable, machine-readable identifier. Switch on this, not on message.
  • message: a readable explanation. Safe to log; do not parse it.
  • param: the request field that caused the error. Present only on some 400 validation errors.
  • estimated_end: an ISO timestamp for when a maintenance window should end. Present only on 503, and only when an end time has been set. The Retry-After header 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:

CodeTypeWhen
missing_api_key / invalid_api_keyauthenticationThe Authorization header is missing, or the key is unknown, revoked, or expired.
insufficient_scopepermissionThe key lacks the endpoint's scope.
invalid_parameters / invalid_jsoninvalid_requestA field failed validation, or the body was not valid JSON.
payload_too_largeinvalid_requestThe request body is over 1 MiB.
unknown_routenot_foundNo endpoint matches the path.
not_foundnot_foundAn id is not a valid UUID, or no record matches it.
method_not_allowedinvalid_requestThe path exists, but not for this method (status 405).
no_availabilityconflictThe room type is sold out for the requested dates.
idempotency_key_reuseconflictAn Idempotency-Key was reused with a different request body.
idempotency_in_progressconflictAn earlier request with the same Idempotency-Key is still running. Retry shortly.
insufficient_pointsconflictA loyalty redemption is more than the available balance.
rate_limitedrate_limitThe per-key or per-IP request limit was exceeded.
too_many_requestsrate_limitToo many promo-code validation attempts.
maintenance_modeserverThe 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_availability for 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.

ParameterDefaultMaxMeaning
limit25100How many records to return.
offset010000How 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.