Skip to content

CBMS Partner API changelog ​

The Partner API is /api/v1.

  • Additive, backward-compatible changes (new optional fields, new endpoints, new webhook event types) ship in v1. Ignore fields you do not recognize.
  • A breaking change (removing or renaming a field, changing a field's meaning or type, changing required parameters) requires a new version prefix. It will not be a silent change to v1.
  • Deprecated behaviour is announced here with a transition period before removal.

The field-level contract is GET /api/v1/openapi.json (unauthenticated).

Sandbox first, production later. The sandbox runs the newest release; production is updated less often. Each entry says where it is live. The current differences are summarised in Sandbox and production differences.


2026-10-30 — the webhook signature sample matches a real delivery ​

Where it is live: sandbox and production.

Changed ​

  • GET /partner/webhooks/{partnerId}/signature-sample: the sample body is now built exactly as a real delivery is. As a booking_created event it carries a hold_id and the details keys that event really has (customer_name, customer_phone, payment_reference). It used to carry price_rupees, which booking_created never sends, and a null hold_id. The headers, the signing formula and the response shape are unchanged; verify the new sample exactly as before.

2026-10-30 — documentation generated from the code: errors, webhook details, limits ​

Where it is live: no behaviour change; applies to both environments.

Documentation corrected (no behaviour change) ​

  • A new Errors page lists every error code a partner can receive, with its statuses.
  • Webhooks, "What details contains": hold_cancelled, booking_cancelled and booking_refunded can also carry shared_slot_states at venues with shared courts; the table did not say so. The table, rate limit, webhook retry and timeout values, signature tolerance, hold durations and key expiry are now generated from the code.
  • The OpenAPI document now also lists base_price_rupees on availability slots (the slot price before the venue's price rules; already returned), 400 VALIDATION_ERROR for a malformed id on GET /bookings/{bookingId}, GET /partner/venues/{venueId} and its resources, and 400 on POST /waitlist. These responses were already returned.
  • The default reason when you send none is "Customer request" for a cancel and "Manager initiated refund" for a refund, whoever refunds. The pages had given "Customer request" as the only example.

2026-10-30 — the contract states length limits the API already enforces ​

Where it is live: no behaviour change; applies to both environments.

Documentation corrected (no behaviour change) ​

  • The OpenAPI document now states limits the API already applied: resource_id, partner_id (hold) and payment_reference (confirm) must not be empty; customer_email is at most 254 characters; webhook endpoint_url is at most 2048 characters; on GET /bookings, customer_name is 2–100 characters, cursor 1–1024, and status may be given one to three times. Requests outside these limits were already refused with 400 VALIDATION_ERROR.

2026-10-30 — documentation corrected: webhook delivery, confirm retries, certification, key handover ​

Where it is live: no behaviour change; applies to both environments.

Documentation corrected (no behaviour change) ​

  • Webhooks are best effort. A notification can arrive late, more than once, out of order, or rarely not at all, and nothing re-sends a lost one. No automated alert fires when a delivery is dead-lettered. Reconcile against GET /bookings; see Webhooks, "Delivery is best effort". The page also now lists what details contains for each event type.
  • Confirm, cancel and refund retries ignore a changed body. A retry with the same key on the same hold or booking replays the original result even if the customer, payment_reference or reason differs. Only a different hold or booking, or the other operation, is 409 IDEMPOTENCY_KEY_MISMATCH. The idempotency page used to say a different customer was refused.
  • Certification is now a list of test cases with pass criteria and the evidence you send. It no longer says plain http webhooks work on the sandbox; they have not since 2026-10-30.
  • Keys are handed to one named engineer through a one-time secret link, and rotation is a manual, overlapping handover (Authentication).

2026-10-30 — partner holds last at least 15 minutes; new keys carry an expiry ​

Where it is live: sandbox and production.

Changed ​

  • POST /bookings/hold: a partner hold now lasts at least 15 minutes, even at a venue whose own hold duration is shorter, so your checkout has time to finish before confirm. A venue's longer duration still applies. hold_expiry_at remains the authoritative deadline; keep reading it.
  • New API keys carry an expiry date, given to you with the key: 90 days on the sandbox now; on production, 365 days once this release reaches it (production keys don't expire by default until then). An expired key answers 401 like a revoked one, with no warning; we contact you before expiry to hand over a replacement (Authentication, "Expiry and rotation"). Existing keys are unchanged.

2026-10-30 — sandbox webhook endpoints follow the production rule ​

Where it is live: sandbox and production.

Changed (sandbox host only) ​

  • POST /partner/webhooks on the sandbox host now applies the same endpoint rule as production: endpoint_url must be https and its host must resolve to a public address, otherwise 400 INVALID_WEBHOOK_ENDPOINT. The same check runs on every delivery. It used to accept plain http and private addresses on the sandbox. To receive sandbox webhooks on your own machine, expose your receiver through a public HTTPS tunnel. Existing sandbox subscriptions to http or private addresses stop receiving events (each delivery fails and is retried like any failed delivery); re-subscribe with an allowed URL. Production is unchanged.

2026-10-30 — credit-funded confirm uses only your packages, and fails closed ​

Where it is live: sandbox and production.

Changed (behaviour change within v1) ​

  • POST /bookings/confirm with use_package_credit: true now deducts a credit only from an active package you sold to that customer (POST /packages/{id}/purchase). A package the customer bought at the venue, on the storefront or through another partner is never used. It used to take the customer's earliest-expiring package at the venue, whoever sold it.
  • When none of your packages has a credit, the confirm is now refused with 409 PACKAGE_CREDIT_UNAVAILABLE: nothing is booked or deducted, and the hold stays active until it expires. It used to answer 201 with package_credit_consumed: false and a booking that nobody had paid for. Sell the customer a package and confirm again, take payment and confirm without use_package_credit, or release the hold.
  • The credit is deducted in the same transaction as the booking. A refusal is not stored against your idempotency_key; a successful confirm replays with package_credit_consumed: true and never deducts twice.
  • Confirms without use_package_credit are unchanged.

2026-10-26 — webhook secrets stay write-once; webhook endpoints must be public https ​

Where it is live: sandbox and production.

Changed ​

  • GET /partner/webhooks no longer includes secret_key in each subscription. The secret is returned once, by POST /partner/webhooks and POST /partner/webhooks/{id}/rotate-secret, as this changelog and the OpenAPI spec already said; the list used to return it anyway. If you lost a secret, rotate it.
  • POST /partner/webhooks: outside the sandbox, endpoint_url must be https and its host must resolve to a public address (not loopback, private, link-local or reserved ranges). Otherwise the request is 400 INVALID_WEBHOOK_ENDPOINT. The same check runs on every delivery, so a subscription whose host later resolves to a non-public address stops receiving events (the delivery fails and is retried like any failed delivery). The sandbox still accepts http and private addresses for local receivers, but never the link-local range.
  • POST /packages/{id}/purchase: the package bought is always the one in the path. A package_id in the body is ignored (it used to override the path).
  • POST /payments/razorpay/order: the amount is the hold's own price. amount_rupees is now optional; when sent it must equal that price, or the request is 400 AMOUNT_MISMATCH (the response carries the correct amount_rupees).

2026-10-21 — holds must start and end on the court's slot grid ​

Where it is live: sandbox and production.

Changed ​

  • POST /bookings/hold: the window (slot_start plus slot_duration_minutes) must start and end on the court's slot grid, as returned by GET /slots/availability. Otherwise 400 SLOT_NOT_AVAILABLE with details.reason: "off_grid" and details.grid_minutes, and nothing is held. Integrations that hold exactly the slot_start and slot_end − slot_start that availability returned are unaffected.

2026-10-05 — waitlist entries are scoped to your partner; confirm and webhook docs corrected ​

Where it is live: sandbox and production.

Changed ​

  • POST /waitlist: a join only ever creates or re-activates your own entry. A customer can be waiting through one channel at a time per venue, resource and date; if another channel (another partner, or the venue's front desk) already has them waiting, the join now returns 409 WAITLIST_ENTRY_CONFLICT and creates or changes nothing. Before, the join matched entries across all partners, so it could return — and re-activate — an entry another channel had made, including that customer's name. A same-partner retry behaves as before.

Documentation corrected (no behaviour change) ​

  • POST /bookings/confirm never returns DOUBLE_BOOKING_ATTEMPT; it was listed by mistake. A held slot that can no longer be booked answers 409 SLOT_NOT_AVAILABLE (its state changed, or the venue blacked it out) or 409 TRANSFORMATION_CONFLICT (a linked court sharing the same space is reserved). With HOLD_EXPIRED, these mean not booked: if you already took the customer's payment, refund or reconcile it on your side. The booking flow guide now says exactly what to do.
  • Webhook partner_id is the subscriber — you — on every delivery, not the channel that caused the change, as the docs had said. Match hold_id / booking_id against your own records to tell your own reservations from other channels'.

2026-10-04 — hold idempotency keys are bound to their request ​

Where it is live: sandbox and production.

Changed ​

  • POST /bookings/hold: reusing an idempotency_key for a different request now returns 409 IDEMPOTENCY_KEY_MISMATCH and holds nothing. "Different" means another venue, slot time, duration (or booking option), court type, or customer. Before, the original hold was replayed as a 201 even when the body described another slot, which handed the caller a hold it had not asked for. A retry of the same request still replays the original hold. The court is compared by type, not number.
  • A key whose hold has expired now returns 409 HOLD_EXPIRED, and one whose hold was cancelled 409 INVALID_HOLD_STATE. Both previously failed with a 500.
  • Holds created before this change replay as before.

This changes v1 behaviour that the idempotency guide described. It ships without a new version prefix because no partner integration is live on Production and the old behaviour returned the wrong hold. Callers that follow the guide (one key per logical hold) see no difference.


2026-10-03 — booking list status filter and paging ​

Where it is live: sandbox and production.

Added ​

  • GET /bookings: optional status (repeatable: BOOKED, CANCELLED, REFUNDED), cursor (keyset paging) and date_basis (utc default | venue_local, which requires venue_id). The response adds next_cursor, has_more and applied_filters.
  • Clients must check applied_filters: older servers ignore unknown parameters and omit it.

Changed ​

  • Rows with the same booked_at are now ordered by booking id (desc), so the order is deterministic. The meaning of from/to is unchanged.

2026-10-03 — availability reasons and cooldown correctness ​

Where it is live: sandbox and production.

Added ​

  • GET /slots/availability: each slot may carry unavailable_reason (booked | held | blackout | past | cooldown) when is_available is false, and the response carries timezone (the IANA zone date was resolved in). Both are additive.

Fixed ​

  • A start blocked by a transformation-rule cooldown (a conflicting resource nearby is held or booked) was reported is_available: true and then refused by POST /bookings/hold with TRANSFORMATION_CONFLICT. Availability now applies the same rule, so such starts are is_available: false with unavailable_reason: "cooldown". Venues without cooldown rules are unaffected.

2026-10-03 — venue timezone fallback and date validation ​

Where it is live: sandbox and production.

Changed ​

  • GET /slots/availability resolves date as the venue's local calendar day (unchanged). For a venue with no timezone configured, that day is now taken in Asia/Kolkata — the same fallback the booking engine already used for holds and notifications — instead of UTC. Venues with a configured timezone are unaffected.

Fixed ​

  • Impossible calendar dates (for example 2026-02-31) are now rejected with 400 VALIDATION_ERROR wherever a YYYY-MM-DD date is accepted (GET /slots/availability, GET /bookings from/to), instead of being rolled into the following month.

2026-09-15 — hold duration documentation corrected ​

Where it is live: sandbox and production.

The CreateHoldResponse shape is unchanged (hold_id, slot_id, status, hold_expiry_at, price_rupees) — this corrects what the docs said about the default value behind hold_expiry_at, which had drifted from what POST /inventory/config actually did.

Changed ​

  • Docs no longer describe hold duration as a vague "~15 minutes." It is set per venue (venue_config.hold_timeout_minutes); the current default for a newly configured venue is 15 minutes, and a venue's operator can change it at any time. hold_expiry_at in the POST /bookings/hold response is always the authoritative value for that specific hold — integrations must read it rather than assume a fixed duration.

Fixed ​

  • POST /inventory/config (venue configuration, manager-only — not a partner-facing endpoint) previously defaulted a brand-new venue's hold duration to a separate hardcoded 10 minutes, contradicting the documented "15 minute application default." New venues now default to the same 15-minute value the booking engine itself falls back to.

Breaking ​

  • None. No partner-facing endpoint, request, or response shape changed.

2026-09-05 — contract hygiene ​

Where it is live: sandbox and production.

Documentation and OpenAPI alignment with the already-implemented Partner API. No new booking behaviour.

Added ​

  • GET /api/v1/openapi.json is the public, unauthenticated OpenAPI document. GET /openapi.json remains as a compatibility alias.
  • OpenAPI servers list production (https://cbms.venuesetu.com/api/v1), sandbox (https://stage.cbms.venuesetu.com/api/v1), and local development.
  • OpenAPI now includes partner-facing waitlist (POST/GET /waitlist, DELETE /waitlist/{id}), package purchase lookup (GET /packages/purchases/{id}, GET /packages/active), hold customer_phone, confirm use_package_credit / package_credit_consumed, booking list filters customer_phone / customer_name, and webhook secret_key on create and rotate responses.
  • OpenAPI info records x-api-version: v1 and the additive-in-v1 policy. This changelog is the version lifecycle.

Changed ​

  • Hold, confirm, and webhook examples no longer send partner_id in the body. Partner identity is the API key.
  • Examples no longer use a pk_live_… key prefix. Issued keys are opaque hex.
  • Webhook event for a confirmed booking is documented as booking_created (not booking_confirmed, which is an internal notification template name).
  • List-webhooks response shape is documented as { success, partner_id, count, data }.
  • Error schema documents request_id and details.
  • Default rate limit is documented: 60 requests / 60 seconds per API key. Webhook delivery: 5 attempts, 10s timeout, backoff 5 / 15 / 30 / 60 minutes, then dead-letter.
  • Idempotency docs distinguish hold creation (same key returns the original hold, including a different slot) from confirm/cancel/refund/package purchase (409 on mismatch).

Fixed ​

  • The documented OpenAPI URL (GET /api/v1/openapi.json) was not mounted. /api/v1 is behind the API-key validator; only GET /openapi.json at the process root actually served the spec. Both URLs now serve the same document without authentication.

Deprecated ​

  • None.

Breaking ​

  • None in this entry. /api/v1 is unchanged as the version prefix.

2026-09-05 — sandbox-first servers ​

Where it is live: sandbox and production.

Documentation metadata only. No endpoint behaviour changed.

Changed ​

  • OpenAPI servers lists sandbox (https://stage.cbms.venuesetu.com/api/v1) first, then production, then local. Sandbox is the recommended integration host; production is for certified partners only.

Previously shipped (before this changelog) ​

Where it is live: sandbox and production.

Recorded so a new partner is not surprised. These were already true of the live Partner API when this file started; dates are not reconstructed.

Added (already live) ​

  • Venue and resource discovery: GET /partner/venues, GET /partner/venues/{venueId}, GET /partner/venues/{venueId}/resources.
  • Partner hold release: POST /partner/holds/{id}/cancel with required idempotency_key.
  • Booking refund: POST /bookings/{id}/refund.
  • Durable per-partner idempotency on hold, confirm, cancel, refund, and package purchase. Redis is a 1-hour fast path; retries after a Redis flush still replay from Postgres. Confirm/cancel/refund/package-purchase reuse of a key for a different logical request is 409. Hold creation returns the original hold (201) even if the reused key is sent with a different slot.
  • Canonical webhook signing: X-CBMS-Signature: sha256=<hex> over {timestamp}.{rawBody}; X-CBMS-Timestamp is the send time of that delivery attempt; 5-minute replay window.
  • Webhook event type hold_cancelled.
  • Isolated sandbox host: https://stage.cbms.venuesetu.com (GET /health → {"sandbox": true}).

Changed (already live) ​

  • partner_id on hold/confirm/webhook bodies is ignored. The API key is the partner identity.
  • Hold creation does not require partner_id in the body (it used to 400 even though the spec never required it).
  • Missing hold/booking ids return 404, not 500.

Fixed (already live) ​

  • OpenAPI availability path is /slots/availability (it previously documented a non-existent /bookings/availability).
  • Webhook signature scheme in the guide matches what the server sends.
  • Partner venue/resource discovery, package purchase, and waitlist join are reachable with a partner API key (a router mount-order bug had required a manager session).

Start in the sandbox. Production access is granted after certification. Access requests and integration support: [email protected]