Launching soon.Get early access
Veridien Docs
API Reference

Conventions

Every endpoint follows the same conventions for status codes, errors, retries, and pagination. Learn them once and they apply everywhere.

StatusMeaning
200Success.
201A resource was created (a guest, a hold, or a charge).
400Invalid request: malformed JSON or a parameter that failed validation.
401Authentication failed: missing or invalid API key.
403The key is valid but lacks the required scope.
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.
429Rate limited. Retry after the Retry-After header.
500Internal server error.
503Maintenance. 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, or server.
  • code: a stable, machine-readable identifier. Switch on this, not on message.
  • message: a human-readable explanation. Safe to log; do not parse.
  • param: the request field that caused the error. Present only on 400 field-validation errors, and omitted otherwise.
  • estimated_end: an ISO timestamp for when a maintenance window is expected to end. Present only on 503, and only when an end time has been set. It is more precise than the Retry-After header, 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:

CodeTypeWhen
missing_api_key / invalid_api_keyauthenticationThe Authorization header is absent or the key is unknown/disabled/expired.
insufficient_scopepermissionThe key lacks the endpoint's scope.
invalid_parameters / invalid_jsoninvalid_requestA field failed validation, or the body was not valid JSON.
unknown_routenot_foundNo endpoint matches the path.
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 exceeds the available balance.
rate_limitedrate_limitThe per-key request limit was exceeded.
maintenance_modeserverThe 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_availability for 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.

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.

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.