Conventions
The conventions below apply to every endpoint in the Operations API. They are modelled on the Stripe API so integrations feel familiar and predictable.
Authentication
Requests authenticate with a site-scoped API key in the Authorization
header:
Authorization: Bearer bf_live_51H8...
bf_live_...keys operate on production data;bf_test_...keys are for the test environment.- Keys are issued and revoked in the BookingFlow dashboard. Only the SHA-256 hash is stored server-side; revocation takes effect immediately.
- A request with a missing key returns
401with"code": "missing_api_key"; an invalid key returns"code": "invalid_api_key". - Every authenticated request resolves to exactly one site — the site the key belongs to. You never pass a site id in the URL; keys cannot read or write another park's data.
App Check is not used on the API (it protects the Firebase client apps, not server-to-server integrations).
Versioning
- The major version is in the path:
/v1/.... - Additive changes (new optional fields, new endpoints, new event types) do not bump the version — code that tolerates unknown fields is future-proof.
- Breaking changes ship as
/v2alongside/v1with a deprecation window. - Responses carry
BookingFlow-Version: v1for debugging.
Data types
| Type | Representation |
|---|---|
| Timestamps | Unix epoch seconds (integers), e.g. 1735689600. Requests accept Unix seconds or ISO 8601. |
| Money | Integers in minor units (pence), e.g. 12345 = £123.45, with a lowercase currency code: "gbp". |
| Calendar dates | ISO YYYY-MM-DD (used in rates and invoice periods, where a park thinks in nights, not instants). |
| Ids | Opaque strings (Firestore document ids). Treat them as case-sensitive cookies. |
Errors
Every failure returns a single envelope with an appropriate HTTP status:
{
"error": {
"type": "invalid_request_error",
"code": "booking_unavailable",
"message": "This accommodation is not available for the selected dates.",
"param": "accommodation",
"doc_url": "https://docs.bookingflow.app/api/bookings"
}
}
type | HTTP | Meaning |
|---|---|---|
invalid_request_error | 400 | Invalid input, failed validation, or an unmet precondition |
authentication_error | 401 | Missing, invalid, or revoked API key |
permission_error | 403 | The key may not perform this operation |
resource_missing | 404 | The referenced object does not exist (or belongs to another site) |
idempotency_error | 409 / 422 | Idempotency conflict (in progress, or key reused with a different body) |
api_error | 500 | Unexpected server error — safe to retry |
code is stable and machine-readable (see the resource reference for the
codes each endpoint returns); param names the offending field when
relevant; doc_url links to the relevant docs page.
Idempotency
All POST endpoints honour the Idempotency-Key header:
- Send a unique key per logical operation (a UUID is fine; a stable name
like
meter-round-2026-11-30is better for agents). - The first execution stores the response; replays with the same key and the
same request body return the stored response with an
Idempotent-Replayed: trueheader — the handler does not run again. - The same key with a different body returns
422(idempotency_key_reused). - If a request with the key is still in flight, a concurrent duplicate gets
409— retry the same request shortly. - Keys expire after 24 hours. Claims on crashed executions expire after 5 minutes. Responses with status ≥ 500 are never stored — the next call re-executes.
Idempotency is also guaranteed where it matters semantically: invoice generation never creates a second invoice for the same closing meter reading, even without a key.
Pagination
List endpoints use cursor pagination:
GET /v1/bookings?limit=25
{
"object": "list",
"url": "/v1/bookings",
"has_more": true,
"data": [ ... ]
}
limit— 1 to 100 (default 25).starting_after— the id of the last object in the previous page.ending_before— the id of the first object in the next page, for paging backwards.has_moretells you whether another page exists. Bookings list newest records in document-id order; events and meter readings list newest-first.
Expansion
Retrieval endpoints that return related ids accept expand[]:
GET /v1/bookings/bk_123?expand[]=accommodation&expand[]=customer
The accommodation and customer fields are replaced with their full
objects inline. Unexpanded, they are plain ids. (Only booking accommodation
and customer are expandable in v1.)
Metadata
Attach up to 50 key/value string pairs to bookings, customers, accommodations, pitches, meter readings, rates and webhook endpoints:
{ "metadata": { "tour": "spring-rally", "segment": "returning" } }
- Keys ≤ 40 chars; values ≤ 500 chars. Numbers and booleans are coerced to strings.
PATCHmerges metadata (DELETEof a single key is not supported in v1 — send the full metadata object you want stored).
Webhooks
Register HTTPS endpoints via Webhook endpoints; every mutation (from the API or the dashboards' shared core logic where wired) emits an event that is POSTed to each enabled endpoint with an HMAC signature:
BookingFlow-Signature: t=1735689600,v1=5257a869e7...
The signature is HMAC-SHA256 over "{t}.{payload}" using the endpoint's
whsec_... secret (returned once at creation). Verify by recomputing and
comparing, and reject timestamps older than five minutes. Delivery is
at-least-once with one attempt per event in v1; record deliveries are
visible on the event objects.
Rate limits
Not enforced in v1 — the API is currently sized for operational (not public-traffic) volumes. Abusive callers can be throttled at the key level; contact support if you need sustained high volume.
Agent notes
The API is deliberately friendly to autonomous callers:
- Send
Idempotency-Keyon every write; derive it from your intent (for examplecreate-booking-{plan-id}), so retries and replays are free. - Bulk endpoints (
meter_readings/bulk,invoices/generate_all,rates/bulk) report per-row outcomes and never fail an entire batch because one row is bad. Fix the reported rows and re-send only those. - Errors carry
code+param+message; use them to self-correct. GET /v1confirms your key, its site, and its mode before a long session.