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
| Environment | Base URL |
|---|---|
| Production | https://api.roaveagents.com/api/v1 |
| Sandbox | Issued 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.
{
"data": { "searchId": "66f1c0a2e4b0c1d2e3f4a5b6" },
"meta": {},
"error": null
}{
"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:
48230inUSDis $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:
| Endpoint | Items | Next cursor | Limit |
|---|---|---|---|
GET /search/:id/results | data | meta.nextCursor | default 20, max 500 |
GET /bookings | data.rows | data.nextCursor | default 50, max 100 |
GET /destinations | data.items | data.nextCursor | default 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.
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.jsonGenerate 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 completed | You 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 running | 409 idempotency_in_progress. Wait and retry. |
on POST /bookings with a different request body | 409 idempotency_in_progress. A key belongs to one request body; use a new key. |
| after the first request returned an error | The 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.
| Endpoint | Requests per minute |
|---|---|
GET /search/:id/results | 300 |
GET /catalog/suggest | 120 |
POST /search/:id/hotels/:productId/rates | 60 |
POST /search/:id/products/:productId/rates | 60 |
POST /search | 30 |
POST /bookings | 20 |
POST /bookings/:id/email-client | 10 |
| Any other endpoint | 120 |
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.
HTTP/1.1 429 Too Many Requests
Retry-After: 41Timeouts and retries
| Call | Client timeout we recommend |
|---|---|
| Search, results, catalog | 30 s |
| Hotel rate sheet, price-check | 30 s |
| Create booking, cancel, pay | 120 s |
- Retry
429,503and network errors with exponential backoff (1 s, 2 s, 4 s…), capped at a few attempts. - Never retry a
4xxother than409 idempotency_in_progress,409 price_check_conflictand429without 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.