Request conventions
JSON in, JSON out, and the handful of rules every public endpoint shares.
Base URL
https://api.ops25.com
All public paths begin /api/public/.
Requests
Send Content-Type: application/json on every write. Anything else is rejected with 415 and
{"error":"Use application/json"}. These rules also appear at the front of the
API reference.
Request bodies are capped at 16 KB. Larger bodies are refused before they are parsed.
The origin rule
The public booking routes, under /api/public/, accept writes only from an allowlisted
Origin. A request with no Origin header, which is to say any request from a server, a script or
a terminal, is refused with 403 and {"error":"Untrusted request origin"}. Reads (GET) are
unaffected: GET /catalog works from anywhere.
The rule does not apply to /api/v1, which is authenticated by an API key instead. To create orders
from a server, use POST /api/v1/orders with a key. See
authentication.
Responses
Success bodies are JSON. Every public response carries Cache-Control: no-store — do not cache
catalogue reads at a CDN and expect pricing changes to reach customers.
Errors
Errors are always the same shape, whatever went wrong:
{ "error": "Enter a valid delivery date." }
The message is written for a person and is safe to show to one. Do not match on its text — match on the status.
| Status | Meaning |
|---|---|
400 |
Malformed JSON. |
402 |
The organisation's subscription is inactive, so online booking is closed. |
403 |
Untrusted origin, or a key that does not match the record. |
404 |
No organisation, or no record, with that identifier. |
415 |
The request was not application/json. |
422 |
Validation failed. The message names the fields, semicolon-separated. |
429 |
Rate limited. Retry after the Retry-After header. |
500 |
Something broke on our side. Safe to retry. |
Rate limits
Public endpoints allow 240 requests per minute, and a limited request comes back 429 with
Retry-After: 60.
The ceiling is currently counted per endpoint across all callers rather than per caller, so treat it
as a shared budget and do not poll /catalog in a loop. Cache it in your own app for a few minutes
instead.
Idempotency
POST /orders accepts an Idempotency-Key header. Replaying a request with the same key against
the same organisation returns the original order rather than creating a second one. Use a fresh UUID per
booking attempt, and reuse it on retries — a dropped response is the case this exists for.