DOCS

Search the developer hub

Jump to a page or an endpoint.

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.