Getting started

Making requests

The conventions every endpoint shares: one base URL, one response shape, cursor pagination, idempotent writes and per-key rate limits.

Base URL and environments

EnvironmentBase URL
Productionhttps://api.roaveagents.com/api/v1
SandboxIssued during onboarding, together with a key for that environment — email support@roaveagents.com.

Send JSON with Content-Type: application/json. Requests must use HTTPS. The API is for server-to-server use and doesn't send CORS headers.

The response envelope

Every JSON response has the same three keys. On success error is null; on failure data is null.

Success · 201
{
  "data": { "searchId": "66f1c0a2e4b0c1d2e3f4a5b6" },
  "meta": {},
  "error": null
}
Failure · 400
{
  "data": null,
  "meta": {},
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "details": ["property guestCount should not exist"]
  }
}

Branch on error.code, not on message — messages are written for people and can change. The Errors page lists every code. The only response that isn't enveloped is the printable voucher document, which is HTML.

Request bodies are strict

A field the endpoint doesn't know is rejected with 400 validation_error rather than ignored, and details names it. This catches typos before they cost you a booking. Responses are the opposite: we add fields without changing the version, so ignore response fields you don't recognise.

IDs, dates and money

  • Search, quote and booking IDs are 24-character hex strings, such as 66f1c0a2e4b0c1d2e3f4a5b6. Hotel IDs (canonicalProductId, canonicalId) use a different format. Treat every ID as an opaque string.
  • Stay and travel dates are YYYY-MM-DD. Timestamps are ISO 8601 in UTC.
  • Money is an integer in the currency's minor unit: 48230 in USD is $482.30. See Money, currency and commission.
  • Countries are ISO 3166-1 alpha-2 codes (GB, AE); currencies are ISO 4217 (USD).

Pagination

Lists use opaque cursors. Pass limit for the page size and the cursor from the previous page to get the next one; a null cursor means there are no more pages. Where the cursor lives depends on the endpoint:

EndpointItemsNext cursorLimit
GET /search/:id/resultsdatameta.nextCursordefault 20, max 500
GET /bookingsdata.rowsdata.nextCursordefault 50, max 100
GET /destinationsdata.itemsdata.nextCursordefault 20, max 100

Don't build or edit cursors yourself. A malformed cursor returns 400 — invalid_cursor on search results, validation_error on the other lists.

Idempotency

Calls that move money or change a booking require an Idempotency-Key header: POST /bookings, POST /bookings/:id/cancel and POST /bookings/:id/pay. Without one you get 400 idempotency_key_required.

Request · cURL
curl -X POST "https://api.roaveagents.com/api/v1/bookings" \
  -H "X-API-Key: $ROAVE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0b6c2f0e-7d1a-4d7e-9a53-2f4b8c1e9d10" \
  -d @booking.json

Generate a fresh UUID for each distinct attempt and store it with your order before you send the request. Keys are scoped to your API key and remembered for 24 hours:

You send the same key…What happens
after the first request completedYou get the original response back. Nothing is booked or charged twice — this is what makes timeouts safe to retry.
while the first request is still running409 idempotency_in_progress. Wait and retry.
on POST /bookings with a different request body409 idempotency_in_progress. A key belongs to one request body; use a new key.
after the first request returned an errorThe request runs again.

If our side fails in the middle of a request, its key can stay "in progress" for up to 24 hours and keep returning 409 idempotency_in_progress. Before you retry with a new key, check GET /bookings for a booking that went through, so you don't book twice.

Rate limits

Limits are counted per API key and per endpoint, in 60-second windows that start with your first request to that endpoint.

EndpointRequests per minute
GET /search/:id/results300
GET /catalog/suggest120
POST /search/:id/hotels/:productId/rates60
POST /search/:id/products/:productId/rates60
POST /search30
POST /bookings20
POST /bookings/:id/email-client10
Any other endpoint120

Responses within the limit carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). Over the limit you get 429 rate_limited with a Retry-After header — wait that many seconds before retrying.

Response · 429 headers
HTTP/1.1 429 Too Many Requests
Retry-After: 41

Timeouts and retries

CallClient timeout we recommend
Search, results, catalog30 s
Hotel rate sheet, price-check30 s
Create booking, cancel, pay120 s
  • Retry 429, 503 and network errors with exponential backoff (1 s, 2 s, 4 s…), capped at a few attempts.
  • Never retry a 4xx other than 409 idempotency_in_progress, 409 price_check_conflict and 429 without changing the request.
  • If a booking call times out on your side, retry it with the same idempotency key. You'll get the real result without booking twice.

Versioning

The version is in the path: /api/v1. Within v1 we only make additive changes — new endpoints, new optional request fields, new response fields and new error codes. Handle unknown values gracefully and you won't need to change anything when we add them.