API reference
The public OPS25 HTTP API. JSON in, JSON out, scoped to one organisation.
The OPS25 API is organised around REST. Resource-oriented URLs, JSON request bodies, JSON responses, and standard HTTP verbs and status codes.
There are two parts to it:
| Part | Paths | Authenticated by | Use it for |
|---|---|---|---|
| The API | /api/v1/… |
A secret API key | Your own systems: reading orders and jobs, creating orders |
| Public booking | /api/public/… |
Nothing, or an allowlisted browser origin | Booking pages and quote tools that run in a browser |
Authentication
API keys
Everything under /api/v1 needs an API key. An owner or admin of the organisation creates keys in
the office under Settings → API Keys. Each key belongs to one organisation, so there is no
organisation in the URL: the key decides whose data you see.
Send the key as a bearer token in the Authorization header:
curl https://api.ops25.com/api/v1/organisation \
-H "Authorization: Bearer ops25_sk_…"
That call returns the organisation the key belongs to, which makes it the quickest check that a key works.
Keys have one of two access levels, chosen when the key is created:
| Access | Can |
|---|---|
| Read only | Every GET under /api/v1 |
| Read and write | Everything a read-only key can, and create orders |
A few rules keep keys safe:
- The key is shown once, when it is created. OPS25 stores only a hash of it, so nobody can show it to you again. If you lose it, revoke it and create another.
- Keep keys on your server. Never put one in a browser, a mobile app or a public repository.
Anyone holding a key can act as the organisation. Every key starts with
ops25_sk_, so secret scanners can spot a leaked one. - Revoking is immediate. A revoked key is refused on its next request with
401. - Use one key per integration, named after it. Settings shows when each key was last used, so you can tell which keys are still in use before revoking any.
- Keys only open
/api/v1. They never reach the office's own routes, and the office session never reaches/api/v1.
A missing, malformed or revoked key gets 401:
{ "error": "This API key is not valid. It may have been revoked." }
A read-only key that tries to write gets 403. When the organisation's subscription has lapsed,
reads keep working and writes get 402.
Public booking routes
The /api/public/{organisation} routes need no key. They name the organisation in the URL by its
booking slug, the same one that appears in its booking page address.
- Reads (
GET) need no credentials and work from a server or a browser. - Writes need an allowlisted
Origin. A request with noOrigin, such as a server, a script or a terminal, is refused with403and{"error":"Untrusted request origin"}. Email developers@ops25.com with the origin you will call from, or use an API key andPOST /api/v1/ordersfrom your server instead.
Requests
The base URL is:
https://api.ops25.com
Send Content-Type: application/json on every write; anything else is 415. Bodies are capped at
16 KB. Lists come back as { "object": "list", "data": [...], "hasMore": false }.
Errors
Errors are always the same shape:
{ "error": "Enter a valid delivery date." }
The message is written for a person and is safe to show to one. Match on the status, not the text.
| Status | Meaning |
|---|---|
400 |
Malformed JSON. |
401 |
The API key is missing, malformed or revoked. |
402 |
The organisation's subscription is inactive, so writes are closed. |
403 |
Untrusted origin, a read-only API key used to write, or a payment 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
Each API key may make 600 requests per minute. Public booking endpoints allow 240 requests per
minute, counted per endpoint across all callers, so treat that as a shared budget. A limited
request comes back 429 with Retry-After: 60. Cache the catalogue in your own app rather than
polling it.
Idempotent requests
Both ways of creating an order accept an Idempotency-Key header. Replaying 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.