Road Ready NZ API

Last updated

OpenAPI 3.1 (JSON): both base URLs and the same calls as this guide, for example to generate a client.

Base URL https://roadreadynz.com/api
Examples

Get started

Check the Road Ready code a traveller gives you when they book, from your own booking system. You need one key and one call.

  1. Create a key On your dashboard, under API Keys.
  2. Keep it on your server Send it with every call.
  3. Verify the code Post the code and your Agency ID.
Environment Base URL
Production https://roadreadynz.com/api
Staging https://staging.roadreadynz.com/api

Staging and production are separate: each has its own keys, and an agency on staging exists only on staging. The switch at the top fills the base URL you choose into every example.

Your API key

Create the key on your dashboard, under API Keys. It verifies Road Ready codes for your agency, and tests your own completion webhook if you have one. Send it on every call, in either header:

HTTP
Authorization: ApiKey <key>
X-API-Key: <key>

Verify a Road Ready code

POST/partner/booking-codes/verifyYour agency key

Checks the code a traveller gives you when they book. Verifying does not use the code up: you can verify the same code again, for example at booking and again at pickup.

Request body

Field Notes
code required The code exactly as the traveller gave it. We normalise it for you (see "How a code is read" below).
companyId required Your Agency ID, shown on your dashboard. Not case-sensitive.
curl -X POST "https://roadreadynz.com/api/partner/booking-codes/verify" \
  -H "Authorization: ApiKey $ROAD_READY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "7K2M9Q", "companyId": "JIM" }'

Response fields

Field Notes
codeRecord Holds these four fields and no others.
createdAt When the code was issued for this agency.
expiresAt The last moment the code verifies: 365 days after it was first issued, so for a code carried over from another agency it comes sooner than createdAt plus 365 days. Always there; null only for a code with no recorded issue date, which does not expire.

Errors

Status When What to do
400 Not six characters of the code alphabet. Ask the traveller to check the code.
404 Not issued for your agency, or mistyped. Ask the traveller to check the code.
410 Issued more than 365 days ago. The traveller takes the guide's quiz again and gets a new code.
400 companyId missing or invalid. Send your Agency ID.
401 The key is wrong or revoked, or companyId is not your Agency ID. Check the key and the Agency ID.
429 Over a rate limit. Wait for Retry-After.
Exact error bodiesverify keeps its own

The verify call keeps the error bodies it has always had, without code, except for 410 and 429. Every other call answers errors as { "error": "...", "code": "..." }.

400 { "isValid": false, "error": "Invalid booking code" }
404 { "isValid": false, "error": "Booking code not found in system" }
410 { "isValid": false, "error": "Booking code has expired", "code": "booking_code_expired" }
400 { "error": "Invalid or missing companyId" }
401 { "error": "Invalid API key" }
403 { "error": "Partner account is suspended" }
429 { "error": "Too many requests. Retry after 40 seconds.", "code": "rate_limited" }

403 is answered to booking platforms only.

How a code is readalphabet and normalisation

A Road Ready code is six characters of this alphabet (Crockford's Base32): the ten digits and the letters A to Z without I, L, O and U.

0123456789ABCDEFGHJKMNPQRSTVWXYZ

Codes are typed by hand, so before a code is checked it is normalised, in this order:

  1. spaces at both ends are removed;
  2. it is converted to upper case;
  3. I and L are read as 1, and O as 0.

A code that is then not six characters of the alphabet answers 400. U is not in the alphabet and is not read as another character, and a space or hyphen inside the code is not removed: both answer 400. Send the code as the traveller gave it. If you check it yourself first, apply the same three steps and test the result against ^[0-9A-HJKMNP-TV-Z]{6}$.

Codes from another agency's guidecarry-over and expiry

A traveller who has completed the guide once gets the same Road Ready code for every agency whose link they open afterwards in the same browser, without watching the video again. The code verifies for each of those agencies.

A code is valid for 365 days from when it was first issued. A code carried over to another agency keeps its first issue date, so carrying it over does not extend it. An older code answers 410 with code booking_code_expired: the traveller takes the guide's quiz again and gets a new code.

Older pathstill works

POST /booking-codes/verify is the same call under its earlier path and keeps working.

Rate limits

Every call counts against your key, per calendar minute (UTC); counts start again at the start of each minute. A call over a limit is answered with 429 and a Retry-After header, the number of seconds until the next minute starts. The body names the limit in code, on the verify call as well: { "error": "Too many requests. Retry after 40 seconds.", "code": "rate_limited" }.

Limit Per minute
Calls with your key, all calls together 120
Failed verifications with your key (400 Invalid booking code, 404 or 410) 10
Calls without a valid key (missing, wrong, revoked or expired key, an Agency ID that is not yours), per IP address 30

Completion webhook

We tell your system the moment a traveller has a Road Ready code, so the booking is marked without anyone typing the code.

Who can have one:

  • A booking platform, for all its agencies: one webhook per platform, with the platform key for the calls below.
  • An agency with its own booking system and no booking platform: one webhook for that agency, with the agency's own key. An agency on a booking platform has none of its own; its events go to the platform's webhook, or nowhere if the platform has none.

Every event goes to one receiver only. The request, the signature and the rules below are the same for both.

It is sent only when the traveller opened the guide through a link that carried your booking reference as ref. A traveller who scans the QR poster or opens the short link has no ref, so nothing is sent for them: verify their code as before. The link may be the guide link or the agency's shortLink with ref added, and both take lang to choose the guide's language:

https://roadreadynz.com/guide?c=<code>&ref=<your reference>
https://roadreadynz.com/guide?c=<code>&lang=<language>&ref=<your reference>

Handling an event

  1. Check the signature Before anything else, over the raw bytes exactly as they arrived (not over a body you parsed and serialised again), and compare in constant time. See Checking the signature.
  2. Match the booking Look the booking up by ref and check that companyId is the agency of that booking's operator. Anyone can change c in a link, so an event can carry another agency's code with your reference.
  3. Verify the code Verify code for that agency with Verify a Road Ready code. The code is valid the moment the webhook arrives. This also makes a replayed request harmless.
  4. Answer 2xx Also for a ref you do not know, and for an eventId you have already handled (store the ones you have), or we keep sending it.

Staff can always verify a code by hand; the webhook does not replace that.

The request

POSTyour URLFrom Road Ready
{
  "code": "ABC234",
  "companyId": "JMY",
  "ref": "0b9f6c1e-7a52-4d0e-9c53-2f1f6f0a8f11",
  "completedAt": "2026-10-13T01:02:03.000Z",
  "eventId": "evt_0123456789abcdef0123456789abcdef"
}
Field Notes
code The traveller's Road Ready code.
companyId The agency code the guide was opened for (the c of the link).
ref Your reference from the link. Always present.
completedAt When the traveller got the code or, with a code they already held, opened your link. ISO 8601 in UTC.
eventId Identifies this event. The same on every repeat of it.
Header Notes
X-RoadReady-Signature sha256= and the HMAC-SHA256 of the raw request body with your signing secret, in lower-case hex.
X-RoadReady-Event-Id The body's eventId.
X-RoadReady-Timestamp When this attempt was sent, ISO 8601 in UTC. For your logs; it is not part of the signature.
User-Agent RoadReadyNZ-Webhook/1

Ignore fields you do not know: we may add some.

Checking the signature

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

function isFromRoadReady(rawBody /* Buffer */, signatureHeader, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const given = Buffer.from(signatureHeader ?? "");
  return given.length === expected.length && timingSafeEqual(given, Buffer.from(expected));
}
Test vectorcheck your code against a known signature

The body {"code":"ABC234","companyId":"JMY","ref":"0b9f6c1e-7a52-4d0e-9c53-2f1f6f0a8f11","completedAt":"2026-10-13T01:02:03.000Z","eventId":"evt_0123456789abcdef0123456789abcdef"} with the secret test-secret-not-a-real-one gives sha256=095c735e8adb4170be60b62dbf2c5c1b01af2bd7c869874870989b8c8a676d9a.

Your answer and retries

Any 2xx means you have it; we do not read the response body. Anything else is a failed attempt and is sent again: 403, 503, every other 4xx and 5xx, a redirect (we do not follow redirects), no answer within 10 seconds, or a connection that fails. After a failed attempt we try again:

  • 1 min after
  • 5 min
  • 15 min
  • 1 h
  • every hour
  • 72 h stop

After 72 hours we stop, unless we send it again by hand after a problem on either side is fixed. Every repeat has the same eventId and the same body. The same booking may also be reported by two events with different codes, for example when two drivers of one booking each complete the guide.

When your endpoint keeps failingthe pause

If your endpoint fails every attempt for 30 minutes or more (at least 5 in a row), we pause: we send one waiting event about once an hour instead of each one on its own schedule, and once you answer it with 2xx the rest follow within minutes, a few at a time. A pause loses nothing, and the 72 hours still apply; an event still waiting at its 72 hour mark is given up with lastError: receiver_paused.

When events are sent

One event is sent for each ref, whichever way the traveller comes to hold a code for that agency:

  • they pass the quiz. The browser remembers the ref of the link for 30 days, so a traveller who opens it today and finishes the guide tomorrow in the same browser is still reported. The guide removes ref from the address once it has it, so do not expect to see it there;
  • they already hold a valid code from another agency's guide, which then counts for this agency as well: the event is sent when they open your link;
  • they already hold a valid code for this agency (a second booking with the same operator, or the email opened again): the event is sent when they open your link, with the code they hold. A ref we have reported is not reported again.
Daily cap per traveller10 refs per traveller and agency in 24 hours

For one traveller and agency we report at most 10 different refs in 24 hours; a link beyond that opens the guide as usual, and nothing is sent for it. This limits one traveller (one browser), not the number of events for an agency: anyone who has a link can complete the guide, so your endpoint must not rely on it to bound what arrives. Check each event as described above, and tell us if you see events you cannot explain: we can switch your webhook off at once.

Test your endpoint

POST/partner/webhook/testYour agency key

Once your webhook is set up, sends a test event to it, whenever you like, on staging and on production.

Request body

Field Notes
agencyCode required Your Agency ID. It tells us which agency the key belongs to, and becomes the event's companyId.
curl -X POST "https://roadreadynz.com/api/partner/webhook/test" \
  -H "Authorization: ApiKey $ROAD_READY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "agencyCode": "JMY" }'

We then send the event like any other: the same body, signature, retries and time-out. Only these tell it apart:

Test event
ref Starts with roadready-test-, then random characters. No guide link can carry this prefix, so a real event never has it.
code Always TEST00. It is never issued to a traveller and never verifies (404).
X-RoadReady-Test Header 1, sent only with a test event.

Every test is a new event with its own eventId. While deliveries to you are paused (see Your answer and retries), a test event waits like any other: it shows pending with attempts: 0 until it is the hour's probe or the pause ends, so a fixed endpoint is found within the hour.

Errors

Status code When
400 invalid_request The body is not a JSON object with only agencyCode, whichever key.
401 invalid_api_key An agency key without agencyCode, or with another agency's code.
409 webhook_not_configured No webhook is set up for you in that environment, also for an agency whose events go to its booking platform. Ask us.
409 webhook_disabled Your webhook is switched off. Ask us.
429 test_limit_exceeded 5 test events per receiver (platform or agency) and clock hour (UTC) already; the sixth waits for Retry-After, the seconds until the next hour.

Both webhook calls also count against your rate limits. Errors are answered as { "error": "...", "code": "..." }: handle code; the wording of error can change.

Follow a delivery

GET/partner/webhook/events/{eventId}?agencyCode=<Agency ID>Your agency key

Shows how the delivery of any of your events is going, a test or a real one. Add your Agency ID as agencyCode.

curl "https://roadreadynz.com/api/partner/webhook/events/evt_5d41402abc4b2a76b9719d911017c592?agencyCode=JMY" \
  -H "Authorization: ApiKey $ROAD_READY_KEY"
Field Notes
status pending (being sent, or waiting for the next attempt), delivered (you answered 2xx) or dead (given up after 72 hours).
attempts Attempts made so far.
lastStatus The HTTP status your endpoint answered last, or null.
lastError Why the last attempt got no status, or null: timeout (no answer within 10 seconds), network (no connection), url_refused (your URL is not acceptable), webhook_off (your webhook is switched off), receiver_paused (given up after 72 hours while deliveries to you were paused), or a fault on our side (no_secret, secret_unavailable, lookup_failed), which we see as well.
nextAttemptAt When the next attempt is due; null once delivered or dead.

The answer never contains code or ref. An event that is not yours answers 404 event_not_found, the same as an unknown id.

Questions, or need a limit raised? Contact us.

Get started

Create a Road Ready agency for each of your operators and show its short link and QR poster in your own product. When travellers book, check their codes for any of your agencies with the same key.

  1. Get your key We issue one per environment. Build against staging first.
  2. Create the agency Store its code next to your externalId.
  3. Show link and poster Or embed the guide in your booking page.
  4. Check codes Verify them, or let the webhook tell you.
Environment Base URL
Production https://roadreadynz.com/api
Staging https://staging.roadreadynz.com/api

Staging and production are separate: each has its own key, and an agency created on staging exists only on staging. The switch at the top fills the base URL you choose into every example.

Call Does
POST /partner/agencies Create an agency
GET /partner/agencies/{code} Read an agency and its poster
GET /partner/agencies List your agencies
PATCH /partner/agencies/{code} Change name, logo or contact email
POST /partner/agencies/{code}/qr/retry Render the poster again
POST /partner/booking-codes/verify Verify a code for one of your agencies
POST your URL Completion webhook (we call you)
POST /partner/webhook/test Send a test event
GET /partner/webhook/events/{eventId} Follow a delivery

Your platform key

We issue it when we set up your platform account. It works for every call on this tab, for every agency created with it. Send it on every call, in either header:

HTTP
Authorization: ApiKey <key>
X-API-Key: <key>

Agencies

Create an agency

POST/partner/agenciesPlatform key

Creates the Road Ready agency of one of your operators.

Request body

Field Notes
agencyName required 1 to 120 characters. Shown to travellers.
externalId required Your own id for the operator, 1 to 128 characters. Unique per platform account.
contactEmail optional Stored for reference. We send no email to it.
logoUrl optional https URL of a PNG, JPEG, WebP or GIF, 5 MB at most. We fetch it once and keep our own copy. See Logo requirements.
curl -X POST "https://roadreadynz.com/api/partner/agencies" \
  -H "Authorization: ApiKey $ROAD_READY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agencyName": "Jimny Rentals",
    "externalId": "op_1234",
    "contactEmail": "ops@jimny.example",
    "logoUrl": "https://cdn.partner.example/jimny.png"
  }'
  • code identifies the agency in every other call, verify included. Store it next to your externalId.
  • shortLink works as soon as the call returns. The QR poster is rendered in the background; see Read an agency and its poster.
  • active is true; see Inactive agencies.

Errors

invalid_request, logo_invalid, logo_fetch_failed, agency_inactive (a repeat for an inactive agency), quota_exceeded (more new agencies in 24 hours than your daily limit). See Error codes.

Read an agency and its poster

GET/partner/agencies/{code}Platform key

Returns the same object as create. Poll it after a create or a logo change until the poster is ready: every 2 to 5 seconds, and stop after 2 minutes. It is normally ready within 60 seconds.

Shell
curl "https://roadreadynz.com/api/partner/agencies/JIM" -H "Authorization: ApiKey $ROAD_READY_KEY"
qrStatus Meaning
pending The poster is being rendered. Ask again in a few seconds.
ready qrUrl is the poster, a PNG (1748 x 2480) with the QR code that opens shortLink.
failed Rendering failed. The agency and shortLink still work. Render it again; if it fails again, contact us.

You can only read agencies created with your key. Any other code returns 404 with agency_not_found.

List your agencies

GET/partner/agenciesPlatform key

Lists the agencies of your platform account, newest first, for example to re-sync your database.

Query parameters

Parameter Notes
limit optional Agencies per page, 1 to 100. Default 50.
cursor optional nextCursor of the previous page. Leave it out for the first page.
externalId optional Your id for one operator. Returns that agency, or an empty list. Cannot be combined with cursor.
curl "https://roadreadynz.com/api/partner/agencies?limit=100" -H "Authorization: ApiKey $ROAD_READY_KEY"
  • Each agency is the same object as GET /partner/agencies/{code} returns.
  • While more agencies exist, nextCursor is a string: pass it unchanged as cursor to get the next page. On the last page it is null. Treat it as opaque; its content can change.
  • A platform account without agencies gets 200 OK with "agencies": [].
  • To find the agency of one operator, GET /partner/agencies?externalId=op_1234 is quicker than paging.

Errors

invalid_request for a limit outside 1 to 100, or a cursor we did not issue to you.

Change name, logo or contact email

PATCH/partner/agencies/{code}Platform key

Changes one or more fields of an agency and returns it as it is after the change (200 OK). code and shortLink never change, so QR codes already printed keep working.

Request body

Field Notes
agencyName optional 1 to 120 characters.
logoUrl optional New logo, same rules as on create (see Logo requirements). null removes the logo.
contactEmail optional Stored for reference, as on create. We send no email to it. null or an empty string removes it. Only for agencies you created; see below.

Send at least one of them. Any other field (code, shortLink, externalId, ...) is refused with invalid_request.

Shell
curl -X PATCH "https://roadreadynz.com/api/partner/agencies/JIM" \
  -H "Authorization: ApiKey $ROAD_READY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "logoUrl": "https://cdn.partner.example/jimny-2026.png" }'

Errors

invalid_request, logo_invalid and logo_fetch_failed (nothing is changed, the name included), agency_not_found (not one of your agencies), agency_inactive, contact_email_not_changeable. See Error codes.

Contact email of agencies that signed up themselves409 contact_email_not_changeable

contactEmail can be changed for agencies you created with POST /partner/agencies. An agency that signed up with us itself and was connected to your account later signs in to its Road Ready dashboard with that address, so for it a request with contactEmail returns 409 with contact_email_not_changeable and changes nothing, the name and logo included. Its name and logo can still be changed in a request without contactEmail.

What counts as a changeno-ops and failed logos

A request that changes nothing (the same name, or null for an agency without a logo) answers 200 and changes nothing. A logoUrl is always fetched again and always renders a new poster. If the new logo fails the checks (logo_invalid, logo_fetch_failed), nothing is changed, the name included.

Render the poster again

POST/partner/agencies/{code}/qr/retryPlatform key

For a poster whose qrStatus is failed, or one that has been pending for more than 5 minutes. No request body.

Shell
curl -X POST "https://roadreadynz.com/api/partner/agencies/JIM/qr/retry" -H "Authorization: ApiKey $ROAD_READY_KEY"

200 OK with the agency object and its current qrStatus:

  • A poster that is pending and started less than 5 minutes ago is being rendered; the call changes nothing and answers pending.
  • Any other poster, ready included, is rendered again and the call answers pending. Poll as after a create.

Errors

agency_not_found for a code that is not one of your agencies, agency_inactive for an inactive agency. See Error codes.

Verify a Road Ready code

POST/partner/booking-codes/verifyPlatform key

One platform key verifies the codes a traveller gives you for all your agencies. Send the agency's code from create as companyId. Verifying does not use the code up: you can verify the same code again, for example at booking and again at pickup.

Request body

Field Notes
code required The code exactly as the traveller gave it. We normalise it for you (see "How a code is read" below).
companyId required The agency's code from create. Not case-sensitive.
curl -X POST "https://roadreadynz.com/api/partner/booking-codes/verify" \
  -H "Authorization: ApiKey $ROAD_READY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "7K2M9Q", "companyId": "JIM" }'

Response fields

Field Notes
codeRecord Holds these four fields and no others.
createdAt When the code was issued for this agency.
expiresAt The last moment the code verifies: 365 days after it was first issued, so for a code carried over from another agency it comes sooner than createdAt plus 365 days. Always there; null only for a code with no recorded issue date, which does not expire.

Errors

Status When What to do
400 Not six characters of the code alphabet. Ask the traveller to check the code.
404 Not issued for this agency, or mistyped. Ask the traveller to check the code.
410 Issued more than 365 days ago. The traveller takes the guide's quiz again and gets a new code.
400 companyId missing or invalid. Send the agency's code.
401 The key is wrong, or companyId is not one of your agencies. Use the code from create.
403 Your platform account is suspended. Contact us.
429 Over a rate limit, for this agency or for the whole key. Wait for Retry-After.
Exact error bodiesverify keeps its own

The verify call keeps the error bodies it has always had, without code, except for 410 and 429. Every other call answers errors as { "error": "...", "code": "..." }.

400 { "isValid": false, "error": "Invalid booking code" }
404 { "isValid": false, "error": "Booking code not found in system" }
410 { "isValid": false, "error": "Booking code has expired", "code": "booking_code_expired" }
400 { "error": "Invalid or missing companyId" }
401 { "error": "Invalid API key" }
403 { "error": "Partner account is suspended" }
429 { "error": "Too many requests. Retry after 40 seconds.", "code": "rate_limited" }

403 is answered to booking platforms only.

How a code is readalphabet and normalisation

A Road Ready code is six characters of this alphabet (Crockford's Base32): the ten digits and the letters A to Z without I, L, O and U.

0123456789ABCDEFGHJKMNPQRSTVWXYZ

Codes are typed by hand, so before a code is checked it is normalised, in this order:

  1. spaces at both ends are removed;
  2. it is converted to upper case;
  3. I and L are read as 1, and O as 0.

A code that is then not six characters of the alphabet answers 400. U is not in the alphabet and is not read as another character, and a space or hyphen inside the code is not removed: both answer 400. Send the code as the traveller gave it. If you check it yourself first, apply the same three steps and test the result against ^[0-9A-HJKMNP-TV-Z]{6}$.

Codes from another agency's guidecarry-over and expiry

A traveller who has completed the guide once gets the same Road Ready code for every agency whose link they open afterwards in the same browser, without watching the video again. The code verifies for each of those agencies.

A code is valid for 365 days from when it was first issued. A code carried over to another agency keeps its first issue date, so carrying it over does not extend it. An older code answers 410 with code booking_code_expired: the traveller takes the guide's quiz again and gets a new code.

Older pathstill works

POST /booking-codes/verify is the same call under its earlier path and keeps working.

Rate limits

Every call counts against your key, per calendar minute (UTC); counts start again at the start of each minute. A call over a limit is answered with 429 and a Retry-After header, the number of seconds until the next minute starts. The body names the limit in code, on the verify call as well: { "error": "Too many requests. Retry after 40 seconds.", "code": "rate_limited" }.

Limit Per minute
Calls with your key, all calls together 120
Failed verifications (400 Invalid booking code, 404 or 410), per agency 10
Failed verifications, all your agencies together 60
Calls without a valid key (missing, wrong, revoked or expired key, suspended platform account, agency that is not yours), per IP address 30
New agencies (quota_exceeded), in 24 hours 50 unless agreed otherwise

One key verifies for all your agencies, so failed verifications are counted for each agency (companyId) separately. Once one agency has used up its 10, verify calls for that agency answer 429, correct codes included, until the minute is over; your other agencies keep verifying. Wrong codes typed at one operator's counter therefore do not stop the others. Only when all your agencies together reach 60 failed verifications in a minute does every verify call with the key answer 429. A 429 looks the same in both cases; Retry-After is the time to wait for that agency, or for the key.

A verify call for a companyId that is not one of your agencies is answered with 401 and counts as a call without a valid key (30 a minute per IP address), not against an agency.

Creating and reading agencies are not held up by failed verifications, only by the 120 calls. A 429 from a create, change or poster retry is recorded under its X-Request-Id like every other one. It is separate from quota_exceeded, the daily limit of new agencies.

Sending travellers to the guide

Guide language

shortLink opens the guide in the language of the traveller's browser. To choose the language yourself, for example from the traveller's licence country, add lang to shortLink:

Guide link
https://roadreadynz.com/?c=JIM&lang=ja-JP
  • en-US English
  • es-ES Spanish
  • pt-BR Portuguese (Brazil)
  • fr-FR French
  • de-DE German
  • sm Samoan
  • to Tongan
  • fil-PH Filipino
  • zh-CN Chinese (Simplified)
  • zh-TW Chinese (Traditional)
  • ja-JP Japanese
  • ko-KR Korean
  • th-TH Thai
  • ar-SA Arabic
Other forms of langregional codes, fallback, the traveller's choice
  • lang also takes any case and a bare or regional language, as the embedded guide does: ja, en-GB, pt-PT, zh-Hant (Traditional Chinese), tl (Filipino).
  • A lang we do not offer opens the guide in the language of the traveller's browser (English if we do not offer that either).
  • The traveller can change the language on the guide's first screen. A browser that has started the guide before keeps the language chosen then, whatever lang says.

Embed the guide

Instead of sending travellers to the short link, you can show the guide inside your own booking page. When the traveller finishes the quiz, the frame hands the Road Ready code to your page: the traveller copies, types or emails nothing. The frame's address is https://roadreadynz.com/embed/<code>?lang=<language>, with the agency's code.

<iframe id="road-ready" src="https://roadreadynz.com/embed/JIM?lang=en"
  allow="clipboard-write" style="width: 100%; height: 720px; border: 0"></iframe>
<script>
  const ROAD_READY = "https://roadreadynz.com";
  const frame = document.getElementById("road-ready");

  window.addEventListener("message", (event) => {
    // Only messages from the guide's frame.
    if (event.origin !== ROAD_READY || event.source !== frame.contentWindow) return;
    const message = event.data;

    if (message.type === "roadready:ready") {
      // Optional: ties the code to this booking; it comes back as `ref`.
      frame.contentWindow.postMessage({ type: "roadready:hello", ref: "BK-1042" }, ROAD_READY);
    }

    if (message.type === "roadready:completed") {
      saveBookingCode(message.ref, message.code).then(() => {
        // Tell the traveller they are done.
        frame.contentWindow.postMessage({ type: "roadready:received", code: message.code }, ROAD_READY);
      });
    }
  });
</script>

Watch the one-minute demo See it working. Our demo booking page (staging) frames the guide, receives the code when the quiz is passed, confirms it and verifies it. The one-minute video shows it, then the same without a confirmation, where the traveller sees their code after 5 seconds.

The messages in detailconfirmation, repeats, origins
  • The frame shows "Done, your rental agency has your Road Ready code" only after roadready:received with the same code. Without it for 5 seconds it shows the code with copy and email buttons, as the normal guide does, so the traveller is never left without their code. A late received still switches to "Done".
  • roadready:completed can arrive more than once for the same code, for example when the traveller reloads the page or after a late hello. Store it idempotently.
  • The frame posts only to your registered origins, never to "*", and ignores messages from anyone but its parent page on a registered origin.
  • Check event.origin on your side as in the example, and send your messages with the Road Ready origin as the target (https://staging.roadreadynz.com on staging).
Good to knowclipboard, frame size, storage
  • Keep allow="clipboard-write" on the iframe. When your page does not confirm a code, the traveller sees it with a copy button. Browsers refuse the clipboard to a frame from another site unless the host page allows it; the guide then tries an older copy method, which some browsers also block in a frame, and the traveller has to select the code by hand.
  • Give the frame at least 360 × 640 pixels. The video plays inline inside the frame, as in the normal guide; there is no full-screen mode.
  • Browsers keep a framed page's storage separate per site. A traveller who took the guide on roadreadynz.com, or in another site's frame, starts again in yours and gets a new code, which verifies like any other.
  • lang takes one of the guide languages in any case, or a bare or regional form such as en, en-GB, ja or zh-Hant (Traditional Chinese); one the guide does not have shows English.
  • The frame is not indexed by search engines and is never cached.

Completion webhook

We tell your system the moment a traveller has a Road Ready code, so the booking is marked without anyone typing the code.

Who can have one:

  • A booking platform, for all its agencies: one webhook per platform, with the platform key for the calls below.
  • An agency with its own booking system and no booking platform: one webhook for that agency, with the agency's own key. An agency on a booking platform has none of its own; its events go to the platform's webhook, or nowhere if the platform has none.

Every event goes to one receiver only. The request, the signature and the rules below are the same for both.

It is sent only when the traveller opened the guide through a link that carried your booking reference as ref. A traveller who scans the QR poster or opens the short link has no ref, so nothing is sent for them: verify their code as before. The link may be the guide link or the agency's shortLink with ref added, and both take lang to choose the guide's language:

https://roadreadynz.com/guide?c=<code>&ref=<your reference>
https://roadreadynz.com/guide?c=<code>&lang=<language>&ref=<your reference>

Handling an event

  1. Check the signature Before anything else, over the raw bytes exactly as they arrived (not over a body you parsed and serialised again), and compare in constant time. See Checking the signature.
  2. Match the booking Look the booking up by ref and check that companyId is the agency of that booking's operator. Anyone can change c in a link, so an event can carry another agency's code with your reference.
  3. Verify the code Verify code for that agency with Verify a Road Ready code. The code is valid the moment the webhook arrives. This also makes a replayed request harmless.
  4. Answer 2xx Also for a ref you do not know, and for an eventId you have already handled (store the ones you have), or we keep sending it.

Staff can always verify a code by hand; the webhook does not replace that.

The request

POSTyour URLFrom Road Ready
{
  "code": "ABC234",
  "companyId": "JMY",
  "ref": "0b9f6c1e-7a52-4d0e-9c53-2f1f6f0a8f11",
  "completedAt": "2026-10-13T01:02:03.000Z",
  "eventId": "evt_0123456789abcdef0123456789abcdef"
}
Field Notes
code The traveller's Road Ready code.
companyId The agency code the guide was opened for (the c of the link).
ref Your reference from the link. Always present.
completedAt When the traveller got the code or, with a code they already held, opened your link. ISO 8601 in UTC.
eventId Identifies this event. The same on every repeat of it.
Header Notes
X-RoadReady-Signature sha256= and the HMAC-SHA256 of the raw request body with your signing secret, in lower-case hex.
X-RoadReady-Event-Id The body's eventId.
X-RoadReady-Timestamp When this attempt was sent, ISO 8601 in UTC. For your logs; it is not part of the signature.
User-Agent RoadReadyNZ-Webhook/1

Ignore fields you do not know: we may add some.

Checking the signature

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

function isFromRoadReady(rawBody /* Buffer */, signatureHeader, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const given = Buffer.from(signatureHeader ?? "");
  return given.length === expected.length && timingSafeEqual(given, Buffer.from(expected));
}
Test vectorcheck your code against a known signature

The body {"code":"ABC234","companyId":"JMY","ref":"0b9f6c1e-7a52-4d0e-9c53-2f1f6f0a8f11","completedAt":"2026-10-13T01:02:03.000Z","eventId":"evt_0123456789abcdef0123456789abcdef"} with the secret test-secret-not-a-real-one gives sha256=095c735e8adb4170be60b62dbf2c5c1b01af2bd7c869874870989b8c8a676d9a.

Your answer and retries

Any 2xx means you have it; we do not read the response body. Anything else is a failed attempt and is sent again: 403, 503, every other 4xx and 5xx, a redirect (we do not follow redirects), no answer within 10 seconds, or a connection that fails. After a failed attempt we try again:

  • 1 min after
  • 5 min
  • 15 min
  • 1 h
  • every hour
  • 72 h stop

After 72 hours we stop, unless we send it again by hand after a problem on either side is fixed. Every repeat has the same eventId and the same body. The same booking may also be reported by two events with different codes, for example when two drivers of one booking each complete the guide.

When your endpoint keeps failingthe pause

If your endpoint fails every attempt for 30 minutes or more (at least 5 in a row), we pause: we send one waiting event about once an hour instead of each one on its own schedule, and once you answer it with 2xx the rest follow within minutes, a few at a time. A pause loses nothing, and the 72 hours still apply; an event still waiting at its 72 hour mark is given up with lastError: receiver_paused.

When events are sent

One event is sent for each ref, whichever way the traveller comes to hold a code for that agency:

  • they pass the quiz. The browser remembers the ref of the link for 30 days, so a traveller who opens it today and finishes the guide tomorrow in the same browser is still reported. The guide removes ref from the address once it has it, so do not expect to see it there;
  • they already hold a valid code from another agency's guide, which then counts for this agency as well: the event is sent when they open your link;
  • they already hold a valid code for this agency (a second booking with the same operator, or the email opened again): the event is sent when they open your link, with the code they hold. A ref we have reported is not reported again.
Daily cap per traveller10 refs per traveller and agency in 24 hours

For one traveller and agency we report at most 10 different refs in 24 hours; a link beyond that opens the guide as usual, and nothing is sent for it. This limits one traveller (one browser), not the number of events for an agency: anyone who has a link can complete the guide, so your endpoint must not rely on it to bound what arrives. Check each event as described above, and tell us if you see events you cannot explain: we can switch your webhook off at once.

Test your endpoint

POST/partner/webhook/testPlatform key

Once your webhook is set up, sends a test event to it, whenever you like, on staging and on production.

Request body

Field Notes
agencyCode optional One of your active agencies. It becomes the event's companyId, so your "is this agency mine" check passes as for a real event. Without it we take your newest active agency (the first active one in the order of GET /partner/agencies).
curl -X POST "https://roadreadynz.com/api/partner/webhook/test" \
  -H "Authorization: ApiKey $ROAD_READY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "agencyCode": "JMY" }'

We then send the event like any other: the same body, signature, retries and time-out. Only these tell it apart:

Test event
ref Starts with roadready-test-, then random characters. No guide link can carry this prefix, so a real event never has it.
code Always TEST00. It is never issued to a traveller and never verifies (404).
X-RoadReady-Test Header 1, sent only with a test event.

Every test is a new event with its own eventId. While deliveries to you are paused (see Your answer and retries), a test event waits like any other: it shows pending with attempts: 0 until it is the hour's probe or the pause ends, so a fixed endpoint is found within the hour.

Errors

Status code When
400 invalid_request The body is not a JSON object with only agencyCode, whichever key.
401 invalid_api_key An agency key without agencyCode, or with another agency's code.
409 webhook_not_configured No webhook is set up for you in that environment, also for an agency whose events go to its booking platform. Ask us.
409 webhook_disabled Your webhook is switched off. Ask us.
429 test_limit_exceeded 5 test events per receiver (platform or agency) and clock hour (UTC) already; the sixth waits for Retry-After, the seconds until the next hour.

Both webhook calls also count against your rate limits. Errors are answered as { "error": "...", "code": "..." }: handle code; the wording of error can change.

Follow a delivery

GET/partner/webhook/events/{eventId}Platform key

Shows how the delivery of any of your events is going, a test or a real one.

curl "https://roadreadynz.com/api/partner/webhook/events/evt_5d41402abc4b2a76b9719d911017c592" \
  -H "Authorization: ApiKey $ROAD_READY_KEY"
Field Notes
status pending (being sent, or waiting for the next attempt), delivered (you answered 2xx) or dead (given up after 72 hours).
attempts Attempts made so far.
lastStatus The HTTP status your endpoint answered last, or null.
lastError Why the last attempt got no status, or null: timeout (no answer within 10 seconds), network (no connection), url_refused (your URL is not acceptable), webhook_off (your webhook is switched off), receiver_paused (given up after 72 hours while deliveries to you were paused), or a fault on our side (no_secret, secret_unavailable, lookup_failed), which we see as well.
nextAttemptAt When the next attempt is due; null once delivered or dead.

The answer never contains code or ref. An event that is not yours answers 404 event_not_found, the same as an unknown id.

Reference

Error codes

Every call except verify answers errors as

JSON
{ "error": "Human-readable message", "code": "machine_readable_code" }

Handle code; the wording of error can change. Verify keeps its own error bodies; see Verify a Road Ready code.

code Status What to do
invalid_request 400 Fix the request. error names the field.
logo_invalid 400 The logo URL or file is not acceptable: not https, not a public host, larger than 5 MB, not a PNG, JPEG, WebP or GIF, or an image that cannot be read (for example a truncated file). No agency was created; on a change, nothing was changed.
logo_fetch_failed 400 We could not download the logo within 8 seconds, or it redirected somewhere we do not follow. No agency was created; on a change, nothing was changed. Retry, or create without logoUrl.
invalid_api_key 401 Key missing, wrong, revoked or expired, or a key of the other environment.
partner_suspended 403 Contact us.
agency_not_found 404 No agency of yours has this code.
event_not_found 404 Webhook event only: no event of yours has this id.
agency_inactive 409 The agency is inactive and cannot be changed. See Inactive agencies; contact us to reactivate it.
contact_email_not_changeable 409 Change only: the agency signed up with us itself, and its contact email is its dashboard login. Send the request without contactEmail.
webhook_not_configured 409 Webhook test only: no webhook is set up for you in this environment, or (agency key) your agency's events go to its booking platform. Ask us.
webhook_disabled 409 Webhook test only: your webhook is switched off. Ask us.
booking_code_expired 410 Verify only: the Road Ready code was issued more than 365 days ago. The traveller takes the quiz again.
quota_exceeded 429 More agencies created in the last 24 hours than your daily limit (50 unless agreed otherwise). Repeated creates of existing agencies still succeed.
test_limit_exceeded 429 Webhook test only: 5 test events this hour already. Wait for Retry-After.
rate_limited 429 Too many calls this minute. Wait for the Retry-After header; see Rate limits.
internal_error 500 Something failed on our side, also while checking your key (your key is not the problem). Retry; a create retried with the same externalId cannot make a duplicate.

Inactive agencies

When an operator stops working with Road Ready, its agency is marked inactive; tell us if one of your operators leaves. An inactive agency:

  • is still returned by read and list, with "active": false;
  • cannot be changed: create (repeated with its externalId), change and poster retry return 409 with agency_inactive, and change nothing;
  • keeps its code and externalId: neither is given to another agency;
  • still verifies the Road Ready codes issued for it. Travellers who open its link still get the guide and a Road Ready code, but that code is not issued for the agency and does not verify for it.

We can make it active again on request; active is then true and everything works as before. Setting an agency to inactive through the API is not available yet.

Logo requirements

  • https on the default port, on a publicly reachable host. Redirects are followed up to three times and must meet the same rules.
  • PNG, JPEG, WebP or GIF, checked by file content (not by the file name or Content-Type), 5 MB at most.
  • A GIF is kept as a still image: its first frame, stored as PNG. An animated GIF does not animate on the poster. A GIF may have 8 megapixels at most (for example 2800 x 2800); a larger one, or the rare one whose PNG is larger than 5 MB, is answered with logo_invalid.
  • The URL is fetched once, during create or change. Later changes to the file are not picked up; send the URL again with PATCH /partner/agencies/{code} to use a new version.

Questions, need a limit raised, or something the API does not do yet? Contact us.