# BookingGood Partner API

Base path: `/api/partner/v1`

## Start with an approved connection

For a club’s own website, start with the ready-made iframe booking widget at /partners#embedding. It uses BookingGood’s booking flow without a custom API backend. This API is for developers building custom booking interfaces, marketplaces or apps. BookingGood owns capacity, club configuration and reservation status. Your service owns checkout, customer communication and financial workflows.

Ask the club administrator for an approved sandbox connection, its API origin, facility grants and a securely delivered credential. Credentials belong to one club and environment. The API is disabled by default; documentation availability does not mean a deployment has enabled it. There is no self-service API key signup.

Call from your backend over HTTPS with Authorization: Bearer <apiCredential>. Keep credentials and the separate callback signing secret out of browser code, URLs and logs. A sales integration needs configuration:read, availability:read, bookings:read and bookings:write. Callbacks also need callbacks:manage and the read scope for each topic.

Independent primary facilities and explicitly granted coaches are supported. Shared resources, recurring creation, courses, accounts, passes and payments are outside this API. Sales pause permits reads and owned future-booking cancellation; full connection disablement also blocks reads, retries and cancellation.

```http
GET /api/partner/v1/connection HTTP/1.1
Authorization: Bearer <apiCredential>
```

## 1. Discover facilities and available times

Read /connection and durably save its original bootstrapCursor before any baseline reads. It advertises scopes, generations, timezone, sales status, horizon and limits. Read /configuration for approved facility IDs and rules.

Fetch /snapshot for explicit facilities and coverage. itemIds is one comma-separated query value. Offer only returned start/end options and quantities up to maxQuantity; empty options and zero capacity cannot be sold. Availability reserves no capacity. Only a committed booking reserves it.

Configuration marks resources as kind facility or coach. Coaches list their granted supplementaryTo facilities. Each facility end option includes availableCoaches for the full interval. A coach needs its own explicit resource grant, working hours and capacity one. Reserve with optional coachId and quantity 1; approval is required if either resource requires it.

Default snapshot limits are seven days and 20 facilities; use the limits advertised by your connection. Split larger coverage into complete windows. IDs and revisions are strings; quantities are positive integer JSON numbers. Timestamps have whole seconds and an explicit offset; responses use UTC Z. Display times in the club timezone. Intervals include the start and exclude the end. Unknown request fields are rejected.

```http
GET /api/partner/v1/snapshot?fromDate=<encodedStart>&toDate=<encodedEnd>&itemIds=42,43 HTTP/1.1
Authorization: Bearer <apiCredential>
```

## 2. Reserve before confirming

Before every mutation, durably save its exact method, path, JSON body, conditional headers, unique Idempotency-Key and BookingGood-Operation-Expires. Choose a future whole-second UTC expiry no more than five minutes ahead of the server clock. Placeholders below are illustrative; choose facility IDs and times from a current snapshot.

Create a reservation with a permanent opaque externalBookingId. This byte-exact, case-sensitive identity survives cancellation and credential rotation and cannot identify a different reservation later. Do not put personal data in it. Optional customer fields are private reservation contacts, omitted from API responses, feeds and callbacks; they do not create accounts or send messages.

Read the returned booking status and retain its ID and exact quoted ETag header. CONFIRMED can be confirmed to your customer. PENDING reserves capacity but awaits staff approval. CANCELED and REJECTED are terminal. Never infer confirmation from availability or an HTTP success alone.

```http
POST /api/partner/v1/bookings HTTP/1.1
Authorization: Bearer <apiCredential>
Content-Type: application/json
Idempotency-Key: <savedUniqueKey>
BookingGood-Operation-Expires: <savedUTCExpiry>

{
  "externalBookingId": "order-7f3a",
  "itemId": "42",
  "fromDate": "<offeredStart>",
  "toDate": "<offeredEnd>",
  "quantity": 1
}
```

## 3. Read, change or cancel

Read /bookings/{id} for current state and ETag, or /bookings/by-external-id?externalBookingId=... for your permanent identity. Encode query values with a URL library: a literal + must be %2B. Only this connection’s owned reservations are visible.

PATCH /bookings/{id} atomically changes a future PENDING or CONFIRMED reservation. Supply only the facility, interval or quantity fields you intend to change. A refused move preserves the original reservation. An edit can require approval again; inspect the returned status.

Omit coachId in a patch to preserve it, supply a coach ID to add or replace it, or send null to remove it. Creation, changes and cancellation update facility and coach capacity together under one booking identity and ETag.

POST /bookings/{id}/cancel cancels a future owned reservation while preserving its identity. The body can be {} or include a reason. Patch and cancel require the current quoted ETag in If-Match plus their own saved idempotency key and expiry. Started reservations need staff reconciliation.

## 4. Resolve timeouts without duplicate bookings

Retry the exact saved request with its original key, expiry, payload and conditional headers until accepted or durably refused. A timeout, expired operation, 404 lookup, authorization failure, 429 or 503 cannot settle an earlier uncertain request. Do not create a new identity or key to escape uncertainty.

Errors use {code, message, requestId}. Only operationFinal: true marks a durably recorded refusal. After a final refusal, a revised attempt uses a new key and expiry. Response decisions remain for at least seven days; permanent external identities survive replay pruning.

For a final 412 revision_mismatch, refresh the ETag and reconsider the edit; for a final 412 capacity_unavailable, refresh availability. 428 means a required precondition is missing. Honor Retry-After on 429 and 503. Expiry fences new admission; it is not a commit deadline.

If checkout is abandoned, keep resolving the outstanding create and conditionally cancel any accepted future reservation with a new operation. A reservation that has started requires staff reconciliation.

## 5. Keep a complete, recoverable baseline

From the saved original bootstrapCursor: fetch configuration, snapshots for every sales facility/window, and every page of the unfiltered owned-booking inventory. Retain terminal identities and withdrawn-facility references. A missing known booking is a recovery incident, not cancellation.

Then replay /changes from that original cursor, using each returned continuation and hasMore. Apply all refreshes for a page durably before saving its cursor, even for an empty filtered page. Never substitute a newer snapshot cursor. Replace only a snapshot’s stated coverage; serialize overlapping refreshes or reject older revisions.

Poll periodically even with healthy callbacks. Callbacks are refresh hints and never advance the feed cursor. bookings.items means refresh those owned IDs; bookings.allOwned means enumerate every owned-booking page. Fetch newly exposed future windows as the horizon advances, even without a callback.

On 410 resync_required or a scope/recovery-generation change, repeat the complete baseline. Administrator sales authorization does not perform synchronization. Coordinate possible data-loss incidents with the operator before resuming sales.

## 6. Verify signed callbacks

Register a singleton subscription with PUT /subscription at an administrator-approved HTTPS hostname on port 443. Use If-None-Match: * for initial creation and the current subscription ETag in If-Match for replacement/resume, plus the saved mutation key and expiry. Redirects and private/reserved destinations are refused.

Verify HMAC-SHA256 over the exact raw body bytes before parsing JSON. Decode the separate 32-byte base64url onboarding signing secret. Join bookinggood.v1, timestamp, eventId, deliveryId, keyId and raw body with LF bytes, with no extra trailing LF. Signature and test vectors are included in the OpenAPI contract.

Headers are BookingGood-Event-Id, BookingGood-Delivery-Id, BookingGood-Signing-Key and BookingGood-Signature: t=<unixSeconds>,v1=<unpaddedBase64urlMac>. Reject malformed or duplicate parameters, unknown versions/keys, noncanonical encodings, payload-ID mismatches and timestamps outside five minutes. Compare MAC bytes in constant time.

For a verified subscription.challenge, echo {"nonce":"<received nonce>"} within ten seconds; the nonce expires after five minutes and binds connection/generation. For events, durably enqueue and deduplicate by stable eventId before returning 2xx within ten seconds. Check generations before applying hints. Retries use fresh delivery IDs and timestamps.

Normal signing-key rotation overlaps keys for 24 hours and switches after receipt confirmation. Compromised keys are revoked immediately. Delivery can suspend after 72 hours of failure; feed recovery remains necessary.
