{
  "openapi": "3.1.0",
  "info": {
    "title": "Road Ready NZ API",
    "version": "1.0.0",
    "summary": "Create Road Ready agencies, read their QR posters and verify Road Ready codes.",
    "description": "For booking platforms that create Road Ready agencies for their operators.\nAn agency that only verifies Road Ready codes uses one call,\n`POST /api/partner/booking-codes/verify`, with a key from its dashboard.\nAn agency with its own booking system and its own completion webhook also\nuses the two Completion webhook calls with that key.\nThe guide at https://roadreadynz.com/developers explains every call, the\nrate limits and the logo requirements; this file describes the requests\nand responses.\n\nStaging and production are separate: each has its own key, and agencies\ncreated on staging exist only on staging. Build against staging first.\n\nEvery response carries an `X-Request-Id` header. Log it and quote it when\nyou report a problem.\n",
    "contact": {
      "name": "Road Ready NZ",
      "url": "https://roadreadynz.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://roadreadynz.com",
      "description": "Production"
    },
    {
      "url": "https://staging.roadreadynz.com",
      "description": "Staging"
    }
  ],
  "security": [
    {
      "ApiKeyAuthorization": []
    },
    {
      "ApiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "Agencies",
      "description": "Agencies created with your platform key."
    },
    {
      "name": "Booking codes",
      "description": "Road Ready codes travellers receive after completing the guide. They were\ncalled booking codes until 14 October 2026: the paths, fields, error codes\nand error texts keep the earlier name.\n"
    },
    {
      "name": "Completion webhook",
      "description": "Test your webhook endpoint and follow the delivery of an event. For a\nbooking platform with its platform key, and for an agency with its own\nbooking system (no platform) with the agency's own key, which then\nnames its agency with `agencyCode`.\n"
    }
  ],
  "paths": {
    "/api/partner/agencies": {
      "post": {
        "tags": [
          "Agencies"
        ],
        "operationId": "createAgency",
        "summary": "Create an agency",
        "description": "Creates an agency for one of your operators. Repeating a create with\nthe same `externalId` is safe: it answers `200` with the agency created\nthe first time and changes nothing (the other fields are ignored). If\nthat agency is inactive, it answers `409` with `agency_inactive`.\nThe QR poster is rendered in the background (`qrStatus: pending`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAgencyRequest"
              },
              "example": {
                "agencyName": "Jimny Rentals",
                "externalId": "op_1234",
                "contactEmail": "ops@jimny.example",
                "logoUrl": "https://cdn.partner.example/jimny.png"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An agency with this `externalId` already exists; it is returned unchanged.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agency"
                },
                "examples": {
                  "readyAgency": {
                    "$ref": "#/components/examples/ReadyAgency"
                  }
                }
              }
            }
          },
          "201": {
            "description": "Agency created.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agency"
                },
                "examples": {
                  "newAgency": {
                    "$ref": "#/components/examples/NewAgency"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequestWithLogo"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/PartnerSuspended"
          },
          "409": {
            "$ref": "#/components/responses/AgencyInactive"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsOrQuota"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "get": {
        "tags": [
          "Agencies"
        ],
        "operationId": "listAgencies",
        "summary": "List your agencies",
        "description": "Your agencies, newest first, paged with an opaque cursor. With\n`externalId` it answers the one matching agency or an empty list.\n",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Agencies per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "`nextCursor` of the previous page, unchanged. Leave it out for the first page.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,200}$"
            }
          },
          {
            "name": "externalId",
            "in": "query",
            "required": false,
            "description": "Your id for one operator. Cannot be combined with `cursor`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of agencies.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgencyList"
                },
                "examples": {
                  "page": {
                    "summary": "A page with more to come",
                    "value": {
                      "agencies": [
                        {
                          "agencyName": "Jimny Rentals",
                          "contactEmail": "ops@jimny.example",
                          "externalId": "op_1234",
                          "code": "JIM",
                          "shortLink": "https://roadreadynz.com/?c=JIM",
                          "qrUrl": "https://firebasestorage.googleapis.com/...",
                          "qrStatus": "ready",
                          "active": true
                        }
                      ],
                      "nextCursor": "eyJjIjoiSklNIn0"
                    }
                  },
                  "empty": {
                    "summary": "A platform account without agencies, or no match for externalId",
                    "value": {
                      "agencies": [],
                      "nextCursor": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/PartnerSuspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/partner/agencies/{code}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AgencyCode"
        }
      ],
      "get": {
        "tags": [
          "Agencies"
        ],
        "operationId": "getAgency",
        "summary": "Read an agency and its QR poster",
        "description": "The same object as create. Poll it while `qrStatus` is `pending`.\nAny code that is not one of your agencies answers `404`.\n",
        "responses": {
          "200": {
            "description": "The agency.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agency"
                },
                "examples": {
                  "readyAgency": {
                    "$ref": "#/components/examples/ReadyAgency"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/PartnerSuspended"
          },
          "404": {
            "$ref": "#/components/responses/AgencyNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "tags": [
          "Agencies"
        ],
        "operationId": "updateAgency",
        "summary": "Change an agency's name, logo or contact email",
        "description": "Send one or more of `agencyName`, `logoUrl` and `contactEmail`; any\nother field is refused with `invalid_request`. `code` and `shortLink` never change. A logo change,\n`null` included, renders the poster again (`qrStatus: pending`). If the\nnew logo fails its checks, nothing is changed. An inactive agency\ncannot be changed (`409`, `agency_inactive`). `contactEmail` can only\nbe changed for an agency created with `POST /partner/agencies`; for an\nagency that signed up itself the request is refused and nothing is\nchanged (`409`, `contact_email_not_changeable`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateAgencyRequest"
              },
              "examples": {
                "newLogo": {
                  "summary": "A new logo",
                  "value": {
                    "logoUrl": "https://cdn.partner.example/jimny-2026.png"
                  }
                },
                "removeLogo": {
                  "summary": "Remove the logo",
                  "value": {
                    "logoUrl": null
                  }
                },
                "rename": {
                  "summary": "A new name",
                  "value": {
                    "agencyName": "Jimny Rentals Queenstown"
                  }
                },
                "newContactEmail": {
                  "summary": "A new contact email",
                  "value": {
                    "contactEmail": "bookings@jimny.example"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The agency as it is after the change.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agency"
                },
                "examples": {
                  "newPosterPending": {
                    "$ref": "#/components/examples/NewPosterPending"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequestWithLogo"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/PartnerSuspended"
          },
          "404": {
            "$ref": "#/components/responses/AgencyNotFound"
          },
          "409": {
            "$ref": "#/components/responses/AgencyNotChangeable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/partner/agencies/{code}/qr/retry": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AgencyCode"
        }
      ],
      "post": {
        "tags": [
          "Agencies"
        ],
        "operationId": "retryAgencyPoster",
        "summary": "Render the QR poster again",
        "description": "For a poster whose `qrStatus` is `failed`, or that has been `pending`\nfor more than 5 minutes. No request body. A poster that is `pending`\nand younger than 5 minutes is left alone; any other is rendered again.\nNot for an inactive agency (`409`, `agency_inactive`).\n",
        "responses": {
          "200": {
            "description": "The agency with its current `qrStatus`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agency"
                },
                "examples": {
                  "newPosterPending": {
                    "$ref": "#/components/examples/NewPosterPending"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/PartnerSuspended"
          },
          "404": {
            "$ref": "#/components/responses/AgencyNotFound"
          },
          "409": {
            "$ref": "#/components/responses/AgencyInactive"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/partner/webhook/test": {
      "post": {
        "tags": [
          "Completion webhook"
        ],
        "operationId": "sendWebhookTestEvent",
        "summary": "Send a test event to your webhook",
        "description": "Writes one test event of the completion webhook for you; we send it\nto your webhook URL like any event (same body, signature, retries\nand time-out), with the extra header `X-RoadReady-Test: 1`. Its `ref`\nstarts with `roadready-test-` and its `code` is `TEST00`, which never\nverifies. `companyId` is the agency you name, or a platform's newest\nactive agency.\n\nWith an agency's own key, `agencyCode` is required and must be that\nagency's code; otherwise `401 invalid_api_key`. A body that is not a\nJSON object with only `agencyCode` is `400 invalid_request`, with\neither key. An agency on a\nbooking platform has no webhook of its own: `409\nwebhook_not_configured`.\n\nAt most 5 test events an hour (UTC clock hour) per receiver\n(platform or agency), on top of the per-minute rate limit. Follow the delivery with\n`GET /api/partner/webhook/events/{eventId}`. Guide: \"Completion\nwebhook\", \"Test your endpoint\".\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookTestRequest"
              },
              "example": {
                "agencyCode": "JIM"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The test event is written; the first attempt follows within seconds.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestAccepted"
                },
                "example": {
                  "eventId": "evt_5d41402abc4b2a76b9719d911017c592"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/PartnerSuspended"
          },
          "404": {
            "$ref": "#/components/responses/AgencyNotFound"
          },
          "409": {
            "$ref": "#/components/responses/WebhookTestRefused"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequestsOrTestLimit"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/partner/webhook/events/{eventId}": {
      "parameters": [
        {
          "name": "eventId",
          "in": "path",
          "required": true,
          "description": "The `eventId` of a test event or of a real event sent to your webhook.",
          "schema": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$"
          }
        }
      ],
      "get": {
        "tags": [
          "Completion webhook"
        ],
        "operationId": "getWebhookEvent",
        "summary": "Read the delivery state of an event",
        "description": "How the delivery of one of your events stands, a test event or a real\none. Never answers `code` or `ref`. An event of another receiver and\nan unknown id are the same `404`. With an agency's own key, name the\nagency with `agencyCode`.\n",
        "parameters": [
          {
            "name": "agencyCode",
            "in": "query",
            "required": false,
            "description": "Only with an agency's own key, and then required: your agency code. Ignored with a platform key.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{1,32}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The delivery state.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEventState"
                },
                "example": {
                  "eventId": "evt_5d41402abc4b2a76b9719d911017c592",
                  "test": true,
                  "status": "pending",
                  "attempts": 1,
                  "lastStatus": 503,
                  "lastError": null,
                  "createdAt": "2026-10-14T01:00:00.000Z",
                  "lastAttemptAt": "2026-10-14T01:00:01.000Z",
                  "nextAttemptAt": "2026-10-14T01:01:01.000Z",
                  "deliveredAt": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/PartnerSuspended"
          },
          "404": {
            "$ref": "#/components/responses/EventNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/partner/booking-codes/verify": {
      "post": {
        "tags": [
          "Booking codes"
        ],
        "operationId": "verifyBookingCode",
        "summary": "Verify a Road Ready code",
        "description": "Verifies a traveller's Road Ready code for one agency. Verifying does not\nuse the code up. Codes and `companyId` are not case-sensitive, and `I`,\n`L` and `O` are read as `1`, `1` and `0`.\n\nAccepts a platform key (for its own agencies) or a key issued for a\nsingle agency. A code is valid for 365 days from its first issue,\nalso after it was carried over to another agency; an older code\nanswers `410`. The error bodies of this endpoint have no `code`,\nexcept for `410` and `429`.\n\nFailed verifications are rate limited. With a platform key they are\ncounted for each agency: a `429` after wrong codes for one agency\ndoes not hold up verifications for the others, until the key as a\nwhole reaches its own limit (guide, \"Rate limits\").\n",
        "requestBody": {
          "$ref": "#/components/requestBodies/VerifyBookingCode"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/VerifyOk"
          },
          "400": {
            "$ref": "#/components/responses/VerifyBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/VerifyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/VerifyForbidden"
          },
          "404": {
            "$ref": "#/components/responses/VerifyNotFound"
          },
          "410": {
            "$ref": "#/components/responses/VerifyExpired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/VerifyInternalError"
          }
        }
      }
    },
    "/api/booking-codes/verify": {
      "post": {
        "tags": [
          "Booking codes"
        ],
        "operationId": "verifyBookingCodeLegacyPath",
        "summary": "Verify a Road Ready code (earlier path)",
        "description": "The same endpoint as `POST /api/partner/booking-codes/verify` under its\nearlier path. It keeps working; new integrations use the `/api/partner/` path.\n",
        "requestBody": {
          "$ref": "#/components/requestBodies/VerifyBookingCode"
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/VerifyOk"
          },
          "400": {
            "$ref": "#/components/responses/VerifyBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/VerifyUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/VerifyForbidden"
          },
          "404": {
            "$ref": "#/components/responses/VerifyNotFound"
          },
          "410": {
            "$ref": "#/components/responses/VerifyExpired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/VerifyInternalError"
          }
        }
      }
    }
  },
  "webhooks": {
    "completion": {
      "post": {
        "tags": [
          "Booking codes"
        ],
        "operationId": "completionWebhook",
        "summary": "A traveller has a Road Ready code (sent to your URL)",
        "description": "We `POST` this to the webhook URL you gave us when a traveller who\nopened the guide through a link with your booking reference\n(`/guide?c=<code>&ref=<your reference>`, or the agency's `shortLink`\nwith `&ref=<your reference>` added) has a Road Ready code: they pass the\nquiz, or they open the link holding a valid code already. Nothing is\nsent without a `ref`, and at most 10 different `ref`s are reported for\none traveller and agency in 24 hours.\n\nCheck `X-RoadReady-Signature` over the raw body before anything else.\nThen check that `companyId` is the agency of the booking `ref` names,\nand verify `code` with `POST /api/partner/booking-codes/verify`.\n\nAny `2xx` is a delivery; the response body is not read. Anything else\n(a `4xx`, a `5xx`, a redirect, no answer within 10 seconds) is retried\nafter 1 minute, 5 minutes, 15 minutes, 1 hour and then hourly, until\n72 hours after the event. After 5 failed attempts in a row over at\nleast 30 minutes, one waiting event is sent about once an hour until\none is answered with `2xx`; then the rest follow. A repeat has the\nsame `eventId` and body.\n",
        "security": [],
        "parameters": [
          {
            "name": "X-RoadReady-Signature",
            "in": "header",
            "required": true,
            "description": "`sha256=` and the HMAC-SHA256 of the raw request body with your signing secret, in lower-case hex.",
            "schema": {
              "type": "string",
              "pattern": "^sha256=[0-9a-f]{64}$"
            },
            "example": "sha256=095c735e8adb4170be60b62dbf2c5c1b01af2bd7c869874870989b8c8a676d9a"
          },
          {
            "name": "X-RoadReady-Event-Id",
            "in": "header",
            "required": true,
            "description": "The body's `eventId`.",
            "schema": {
              "type": "string",
              "pattern": "^evt_[0-9a-f]{32}$"
            }
          },
          {
            "name": "X-RoadReady-Timestamp",
            "in": "header",
            "required": true,
            "description": "When this attempt was sent. Not part of the signature.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "RoadReadyNZ-Webhook/1"
            }
          },
          {
            "name": "X-RoadReady-Test",
            "in": "header",
            "required": false,
            "description": "Only on a test event you asked for with `POST /api/partner/webhook/test`. Not part of the signature: rely on the `ref` prefix `roadready-test-`, which is signed, to tell a test from a real event.",
            "schema": {
              "type": "string",
              "const": "1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompletionEvent"
              },
              "example": {
                "code": "ABC234",
                "companyId": "JMY",
                "ref": "0b9f6c1e-7a52-4d0e-9c53-2f1f6f0a8f11",
                "completedAt": "2026-10-13T01:02:03.000Z",
                "eventId": "evt_0123456789abcdef0123456789abcdef"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "You have the event, also when you do not know the `ref`. It is not sent again."
          },
          "4XX": {
            "description": "A failed attempt, a refused signature (`403`) included. Sent again."
          },
          "5XX": {
            "description": "A failed attempt. Sent again."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuthorization": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "The key with the `ApiKey` prefix: `Authorization: ApiKey <key>`."
      },
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "The key on its own: `X-API-Key: <key>`."
      }
    },
    "parameters": {
      "AgencyCode": {
        "name": "code",
        "in": "path",
        "required": true,
        "description": "The agency's `code`, as returned by create. Not case-sensitive.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9]{1,32}$"
        },
        "example": "JIM"
      }
    },
    "headers": {
      "X-Request-Id": {
        "description": "Id of this request. Log it and quote it when you report a problem.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "Retry-After": {
        "description": "Seconds until the next minute (UTC) starts and the counts start again.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 60
        }
      }
    },
    "schemas": {
      "Agency": {
        "type": "object",
        "description": "An agency of your platform account.",
        "required": [
          "agencyName",
          "contactEmail",
          "externalId",
          "code",
          "shortLink",
          "qrUrl",
          "qrStatus",
          "active"
        ],
        "properties": {
          "agencyName": {
            "type": "string",
            "description": "Shown to travellers."
          },
          "contactEmail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Stored for reference; no email is sent to it."
          },
          "externalId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own id for the operator. Unique per platform account."
          },
          "code": {
            "type": "string",
            "description": "Identifies the agency in every other call. Never changes."
          },
          "shortLink": {
            "type": "string",
            "format": "uri",
            "description": "The link travellers open. Works as soon as create returns. Never changes. Append `&lang=<language>` to choose the guide language (guide, \"Guide language\")."
          },
          "qrUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The QR poster (PNG). Every render gets a new URL; while a new\nposter is `pending` this is still the previous one, if any.\n"
          },
          "qrStatus": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed"
            ],
            "description": "`pending`: being rendered; `ready`: `qrUrl` is the poster; `failed`: render it again with `qr/retry`."
          },
          "active": {
            "type": "boolean",
            "description": "`false` once we have marked the agency inactive. It can still be\nread and its Road Ready codes still verify, but it cannot be changed\n(`agency_inactive`). See the guide.\n"
          }
        }
      },
      "AgencyList": {
        "type": "object",
        "required": [
          "agencies",
          "nextCursor"
        ],
        "properties": {
          "agencies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Agency"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass it unchanged as `cursor` for the next page; `null` on the last page. Opaque."
          }
        }
      },
      "CreateAgencyRequest": {
        "type": "object",
        "required": [
          "agencyName",
          "externalId"
        ],
        "properties": {
          "agencyName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "externalId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "Your own id for the operator. Unique per platform account; a repeated create with it returns the existing agency."
          },
          "contactEmail": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 320
          },
          "logoUrl": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2048,
            "format": "uri",
            "description": "`https` URL of a PNG, JPEG, WebP or GIF (kept as a still of its first frame), 5 MB at most. Fetched once; see the guide's logo requirements."
          }
        }
      },
      "UpdateAgencyRequest": {
        "type": "object",
        "minProperties": 1,
        "additionalProperties": false,
        "properties": {
          "agencyName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "logoUrl": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2048,
            "format": "uri",
            "description": "A new logo, same rules as on create; `null` removes the logo."
          },
          "contactEmail": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 320,
            "description": "Stored for reference, as on create; `null` or an empty string removes it. Only for an agency created with `POST /partner/agencies` (`contact_email_not_changeable` otherwise)."
          }
        }
      },
      "PartnerError": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable; its wording can change. Handle `code`."
          },
          "code": {
            "$ref": "#/components/schemas/PartnerErrorCode"
          }
        }
      },
      "PartnerErrorCode": {
        "type": "string",
        "enum": [
          "invalid_request",
          "logo_invalid",
          "logo_fetch_failed",
          "invalid_api_key",
          "partner_suspended",
          "agency_not_found",
          "agency_inactive",
          "contact_email_not_changeable",
          "webhook_not_configured",
          "webhook_disabled",
          "event_not_found",
          "booking_code_expired",
          "quota_exceeded",
          "test_limit_exceeded",
          "rate_limited",
          "internal_error"
        ],
        "description": "| Status | code |\n|---|---|\n| 400 | `invalid_request`, `logo_invalid`, `logo_fetch_failed` |\n| 401 | `invalid_api_key` |\n| 403 | `partner_suspended` |\n| 404 | `agency_not_found`, `event_not_found` (webhook event only) |\n| 409 | `agency_inactive` (create, change, poster retry, webhook test), `contact_email_not_changeable` (change only), `webhook_not_configured` and `webhook_disabled` (webhook test only) |\n| 410 | `booking_code_expired` (verify only) |\n| 429 | `quota_exceeded` (create only), `test_limit_exceeded` (webhook test only), `rate_limited` |\n| 500 | `internal_error` |\n"
      },
      "RateLimitedError": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "const": "rate_limited"
          }
        }
      },
      "CompletionEvent": {
        "type": "object",
        "description": "Body of the completion webhook. More fields may be added; ignore the ones you do not know.",
        "required": [
          "code",
          "companyId",
          "ref",
          "completedAt",
          "eventId"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "The traveller's Road Ready code."
          },
          "companyId": {
            "type": "string",
            "description": "The `code` of the agency the guide was opened for."
          },
          "ref": {
            "type": "string",
            "pattern": "^[A-Za-z0-9._~-]{1,128}$",
            "description": "Your booking reference, as it was in the guide link."
          },
          "completedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the traveller got the code or, with a code they already held, opened your link. In UTC."
          },
          "eventId": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "Identifies the event. The same on every repeat of it."
          }
        }
      },
      "WebhookTestRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "agencyCode": {
            "type": "string",
            "description": "With a platform key: one of your active agencies, sent as the event's `companyId`; left out, your newest active agency. With an agency's own key: required, your agency code."
          }
        }
      },
      "WebhookTestAccepted": {
        "type": "object",
        "required": [
          "eventId"
        ],
        "properties": {
          "eventId": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "The test event's id, also its `eventId` in the webhook body."
          }
        }
      },
      "WebhookEventState": {
        "type": "object",
        "required": [
          "eventId",
          "test",
          "status",
          "attempts",
          "lastStatus",
          "lastError",
          "createdAt",
          "lastAttemptAt",
          "nextAttemptAt",
          "deliveredAt"
        ],
        "properties": {
          "eventId": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$"
          },
          "test": {
            "type": "boolean",
            "description": "Whether this is a test event."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "dead"
            ],
            "description": "`pending`: being sent or waiting for the next attempt. `delivered`: your endpoint answered `2xx`. `dead`: given up after 72 hours."
          },
          "attempts": {
            "type": "integer",
            "minimum": 0
          },
          "lastStatus": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The HTTP status your endpoint answered on the last attempt."
          },
          "lastError": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the last attempt got no HTTP status, e.g. `timeout`, `network`, `url_refused`, `webhook_off` or `receiver_paused`. See the guide."
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastAttemptAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "nextAttemptAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Only while `pending`."
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "VerifyBookingCodeRequest": {
        "type": "object",
        "required": [
          "code",
          "companyId"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "The Road Ready code as the traveller gave it. Once normalised (spaces at both ends removed, upper case, `I` and `L` read as `1`, `O` as `0`) it is six characters of `0123456789ABCDEFGHJKMNPQRSTVWXYZ`. See the guide, \"Verify a Road Ready code\"."
          },
          "companyId": {
            "type": "string",
            "description": "The agency's `code`."
          }
        }
      },
      "VerifyBookingCodeResult": {
        "type": "object",
        "required": [
          "isValid",
          "codeRecord"
        ],
        "properties": {
          "isValid": {
            "type": "boolean",
            "const": true
          },
          "codeRecord": {
            "type": "object",
            "description": "These four fields and no others.",
            "required": [
              "code",
              "status",
              "createdAt",
              "expiresAt"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "createdAt": {
                "type": "string",
                "format": "date-time",
                "description": "When the code was issued for this agency."
              },
              "expiresAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "The last moment the code verifies: 365 days after it was first issued,\nwhich for a code carried over from another agency is earlier than\n`createdAt` plus 365 days. After it, verify answers `410`\n`booking_code_expired`. Always present; `null` only for a code with no\nrecorded issue date, which does not expire.\n"
              }
            }
          }
        }
      },
      "VerifyInvalid": {
        "type": "object",
        "required": [
          "isValid",
          "error"
        ],
        "properties": {
          "isValid": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "VerifyExpiredError": {
        "type": "object",
        "required": [
          "isValid",
          "error",
          "code"
        ],
        "properties": {
          "isValid": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "const": "booking_code_expired"
          }
        }
      },
      "VerifyError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    },
    "requestBodies": {
      "VerifyBookingCode": {
        "required": true,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/VerifyBookingCodeRequest"
            },
            "example": {
              "code": "7K2M9Q",
              "companyId": "JIM"
            }
          }
        }
      }
    },
    "examples": {
      "NewAgency": {
        "summary": "Just created; the poster is being rendered",
        "value": {
          "agencyName": "Jimny Rentals",
          "contactEmail": "ops@jimny.example",
          "externalId": "op_1234",
          "code": "JIM",
          "shortLink": "https://roadreadynz.com/?c=JIM",
          "qrUrl": null,
          "qrStatus": "pending",
          "active": true
        }
      },
      "ReadyAgency": {
        "summary": "The poster is ready",
        "value": {
          "agencyName": "Jimny Rentals",
          "contactEmail": "ops@jimny.example",
          "externalId": "op_1234",
          "code": "JIM",
          "shortLink": "https://roadreadynz.com/?c=JIM",
          "qrUrl": "https://firebasestorage.googleapis.com/...",
          "qrStatus": "ready",
          "active": true
        }
      },
      "NewPosterPending": {
        "summary": "A new poster is being rendered; qrUrl is still the previous one",
        "value": {
          "agencyName": "Jimny Rentals",
          "contactEmail": "ops@jimny.example",
          "externalId": "op_1234",
          "code": "JIM",
          "shortLink": "https://roadreadynz.com/?c=JIM",
          "qrUrl": "https://firebasestorage.googleapis.com/...",
          "qrStatus": "pending",
          "active": true
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`invalid_request`: fix the request; `error` names the field.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "example": {
              "error": "limit must be a whole number from 1 to 100.",
              "code": "invalid_request"
            }
          }
        }
      },
      "BadRequestWithLogo": {
        "description": "`invalid_request`: fix the request. `logo_invalid`: the logo URL or\nfile is not acceptable. `logo_fetch_failed`: the logo could not be\ndownloaded. Nothing was created or changed.\n",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "examples": {
              "invalidRequest": {
                "value": {
                  "error": "agencyName is required, 1 to 120 characters.",
                  "code": "invalid_request"
                }
              },
              "logoInvalid": {
                "value": {
                  "error": "Logo must be smaller than 5MB.",
                  "code": "logo_invalid"
                }
              },
              "logoFetchFailed": {
                "value": {
                  "error": "Fetching the logo timed out.",
                  "code": "logo_fetch_failed"
                }
              }
            }
          }
        }
      },
      "InvalidApiKey": {
        "description": "`invalid_api_key`: key missing, wrong, revoked or expired, or a key of the other environment.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "example": {
              "error": "Invalid API key.",
              "code": "invalid_api_key"
            }
          }
        }
      },
      "PartnerSuspended": {
        "description": "`partner_suspended`: contact us.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "example": {
              "error": "This partner account is suspended.",
              "code": "partner_suspended"
            }
          }
        }
      },
      "AgencyNotFound": {
        "description": "`agency_not_found`: no agency of yours has this code.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "example": {
              "error": "Agency not found.",
              "code": "agency_not_found"
            }
          }
        }
      },
      "AgencyInactive": {
        "description": "`agency_inactive`: we have marked this agency inactive; it cannot be changed. Contact us.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "example": {
              "error": "This agency is inactive and cannot be changed. Contact us to reactivate it.",
              "code": "agency_inactive"
            }
          }
        }
      },
      "AgencyNotChangeable": {
        "description": "`agency_inactive`: we have marked this agency inactive; it cannot be changed. Contact us. `contact_email_not_changeable`: the agency signed up itself and its contact email is its dashboard login; send the request without `contactEmail`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "examples": {
              "agencyInactive": {
                "value": {
                  "error": "This agency is inactive and cannot be changed. Contact us to reactivate it.",
                  "code": "agency_inactive"
                }
              },
              "contactEmailNotChangeable": {
                "value": {
                  "error": "This agency has its own dashboard login and its contactEmail is that login. It cannot be changed here.",
                  "code": "contact_email_not_changeable"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited`: too many calls this minute. Wait `Retry-After` seconds.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RateLimitedError"
            },
            "example": {
              "error": "Too many requests. Retry after 40 seconds.",
              "code": "rate_limited"
            }
          }
        }
      },
      "TooManyRequestsOrQuota": {
        "description": "`rate_limited`: too many calls this minute; wait `Retry-After` seconds\n(the header is sent with `rate_limited` only). `quota_exceeded`: more\nagencies created in the last 24 hours than your daily limit.\n",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "examples": {
              "rateLimited": {
                "value": {
                  "error": "Too many requests. Retry after 40 seconds.",
                  "code": "rate_limited"
                }
              },
              "quotaExceeded": {
                "value": {
                  "error": "Daily limit of new agencies reached.",
                  "code": "quota_exceeded"
                }
              }
            }
          }
        }
      },
      "WebhookTestRefused": {
        "description": "`webhook_not_configured`: no webhook is set up for you in this environment, or (agency key) your agency's events go to its booking platform. `webhook_disabled`: your webhook is switched off. Ask us in both cases. `agency_inactive`: the agency is inactive.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "examples": {
              "webhookNotConfigured": {
                "value": {
                  "error": "No webhook is set up for your platform in this environment. Ask us to set one up.",
                  "code": "webhook_not_configured"
                }
              },
              "webhookDisabled": {
                "value": {
                  "error": "Your webhook is switched off in this environment. Ask us to switch it on.",
                  "code": "webhook_disabled"
                }
              },
              "agencyInactive": {
                "value": {
                  "error": "This agency is inactive and cannot be changed. Contact us to reactivate it.",
                  "code": "agency_inactive"
                }
              }
            }
          }
        }
      },
      "TooManyRequestsOrTestLimit": {
        "description": "`rate_limited`: too many calls this minute. `test_limit_exceeded`: 5\ntest events this hour already. Both carry `Retry-After`.\n",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "examples": {
              "rateLimited": {
                "value": {
                  "error": "Too many requests. Retry after 40 seconds.",
                  "code": "rate_limited"
                }
              },
              "testLimitExceeded": {
                "value": {
                  "error": "At most 5 test events an hour. Retry after 1200 seconds.",
                  "code": "test_limit_exceeded"
                }
              }
            }
          }
        }
      },
      "EventNotFound": {
        "description": "`event_not_found`: no event of yours has this id.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "example": {
              "error": "Event not found.",
              "code": "event_not_found"
            }
          }
        }
      },
      "InternalError": {
        "description": "`internal_error`: something failed on our side, also while checking the key (not a key problem). Retry; a create retried with the same `externalId` cannot make a duplicate.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PartnerError"
            },
            "example": {
              "error": "Failed to create the agency.",
              "code": "internal_error"
            }
          }
        }
      },
      "VerifyOk": {
        "description": "The code was issued for this agency.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/VerifyBookingCodeResult"
            },
            "example": {
              "isValid": true,
              "codeRecord": {
                "code": "7K2M9Q",
                "status": "active",
                "createdAt": "2026-09-27T01:02:03.000Z",
                "expiresAt": "2027-09-27T01:02:03.000Z"
              }
            }
          }
        }
      },
      "VerifyBadRequest": {
        "description": "A malformed Road Ready code (`isValid` false), or a missing or invalid field.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/VerifyInvalid"
                },
                {
                  "$ref": "#/components/schemas/VerifyError"
                }
              ]
            },
            "examples": {
              "malformedCode": {
                "summary": "Not six characters of the code alphabet",
                "value": {
                  "isValid": false,
                  "error": "Invalid booking code"
                }
              },
              "missingCompanyId": {
                "value": {
                  "error": "Invalid or missing companyId"
                }
              }
            }
          }
        }
      },
      "VerifyUnauthorized": {
        "description": "The key is missing or wrong, or the agency is not one of yours.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/VerifyError"
            },
            "example": {
              "error": "Invalid API key"
            }
          }
        }
      },
      "VerifyForbidden": {
        "description": "The platform account is suspended.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/VerifyError"
            },
            "example": {
              "error": "Partner account is suspended"
            }
          }
        }
      },
      "VerifyNotFound": {
        "description": "Not issued for this agency, or mistyped.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/VerifyInvalid"
            },
            "example": {
              "isValid": false,
              "error": "Booking code not found in system"
            }
          }
        }
      },
      "VerifyExpired": {
        "description": "Issued for this agency, but more than 365 days ago (counted from the\ncode's first issue, also after a carry-over). The traveller takes\nthe guide's quiz again to get a new code. Counts as a failed\nverification for the rate limit.\n",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/VerifyExpiredError"
            },
            "example": {
              "isValid": false,
              "error": "Booking code has expired",
              "code": "booking_code_expired"
            }
          }
        }
      },
      "VerifyInternalError": {
        "description": "Retry.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/VerifyError"
            },
            "example": {
              "error": "Internal server error"
            }
          }
        }
      }
    }
  }
}
