Appearance
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 samplebodyis now built exactly as a real delivery is. As abooking_createdevent it carries ahold_idand thedetailskeys that event really has (customer_name,customer_phone,payment_reference). It used to carryprice_rupees, whichbooking_creatednever sends, and a nullhold_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
errorcode a partner can receive, with its statuses. - Webhooks, "What
detailscontains":hold_cancelled,booking_cancelledandbooking_refundedcan also carryshared_slot_statesat 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_rupeeson availability slots (the slot price before the venue's price rules; already returned),400 VALIDATION_ERRORfor a malformed id onGET /bookings/{bookingId},GET /partner/venues/{venueId}and its resources, and400onPOST /waitlist. These responses were already returned. - The default
reasonwhen 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) andpayment_reference(confirm) must not be empty;customer_emailis at most 254 characters; webhookendpoint_urlis at most 2048 characters; onGET /bookings,customer_nameis 2–100 characters,cursor1–1024, andstatusmay be given one to three times. Requests outside these limits were already refused with400 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 whatdetailscontains 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_referenceorreasondiffers. Only a different hold or booking, or the other operation, is409 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
httpwebhooks 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_atremains 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
401like 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/webhookson the sandbox host now applies the same endpoint rule as production:endpoint_urlmust behttpsand its host must resolve to a public address, otherwise 400INVALID_WEBHOOK_ENDPOINT. The same check runs on every delivery. It used to accept plainhttpand private addresses on the sandbox. To receive sandbox webhooks on your own machine, expose your receiver through a public HTTPS tunnel. Existing sandbox subscriptions tohttpor 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/confirmwithuse_package_credit: truenow 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 withpackage_credit_consumed: falseand a booking that nobody had paid for. Sell the customer a package and confirm again, take payment and confirm withoutuse_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 withpackage_credit_consumed: trueand never deducts twice. - Confirms without
use_package_creditare 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/webhooksno longer includessecret_keyin each subscription. The secret is returned once, byPOST /partner/webhooksandPOST /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_urlmust behttpsand its host must resolve to a public address (not loopback, private, link-local or reserved ranges). Otherwise the request is 400INVALID_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 acceptshttpand 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. Apackage_idin the body is ignored (it used to override the path).POST /payments/razorpay/order: the amount is the hold's own price.amount_rupeesis now optional; when sent it must equal that price, or the request is 400AMOUNT_MISMATCH(the response carries the correctamount_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_startplusslot_duration_minutes) must start and end on the court's slot grid, as returned byGET /slots/availability. Otherwise 400SLOT_NOT_AVAILABLEwithdetails.reason: "off_grid"anddetails.grid_minutes, and nothing is held. Integrations that hold exactly theslot_startandslot_end − slot_startthat 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 409WAITLIST_ENTRY_CONFLICTand 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/confirmnever returnsDOUBLE_BOOKING_ATTEMPT; it was listed by mistake. A held slot that can no longer be booked answers 409SLOT_NOT_AVAILABLE(its state changed, or the venue blacked it out) or 409TRANSFORMATION_CONFLICT(a linked court sharing the same space is reserved). WithHOLD_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_idis the subscriber — you — on every delivery, not the channel that caused the change, as the docs had said. Matchhold_id/booking_idagainst 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 anidempotency_keyfor a different request now returns 409IDEMPOTENCY_KEY_MISMATCHand holds nothing. "Different" means another venue, slot time, duration (or booking option), court type, or customer. Before, the original hold was replayed as a201even 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 409INVALID_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: optionalstatus(repeatable:BOOKED,CANCELLED,REFUNDED),cursor(keyset paging) anddate_basis(utcdefault |venue_local, which requiresvenue_id). The response addsnext_cursor,has_moreandapplied_filters.- Clients must check
applied_filters: older servers ignore unknown parameters and omit it.
Changed
- Rows with the same
booked_atare now ordered by booking id (desc), so the order is deterministic. The meaning offrom/tois unchanged.
2026-10-03 — availability reasons and cooldown correctness
Where it is live: sandbox and production.
Added
GET /slots/availability: each slot may carryunavailable_reason(booked|held|blackout|past|cooldown) whenis_availableis false, and the response carriestimezone(the IANA zonedatewas 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: trueand then refused byPOST /bookings/holdwithTRANSFORMATION_CONFLICT. Availability now applies the same rule, so such starts areis_available: falsewithunavailable_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/availabilityresolvesdateas the venue's local calendar day (unchanged). For a venue with no timezone configured, that day is now taken inAsia/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 with400 VALIDATION_ERRORwherever aYYYY-MM-DDdate is accepted (GET /slots/availability,GET /bookingsfrom/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_atin thePOST /bookings/holdresponse 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.jsonis the public, unauthenticated OpenAPI document.GET /openapi.jsonremains as a compatibility alias.- OpenAPI
serverslist 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), holdcustomer_phone, confirmuse_package_credit/package_credit_consumed, booking list filterscustomer_phone/customer_name, and webhooksecret_keyon create and rotate responses. - OpenAPI
inforecordsx-api-version: v1and the additive-in-v1 policy. This changelog is the version lifecycle.
Changed
- Hold, confirm, and webhook examples no longer send
partner_idin 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(notbooking_confirmed, which is an internal notification template name). - List-webhooks response shape is documented as
{ success, partner_id, count, data }. - Error schema documents
request_idanddetails. - 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/v1is behind the API-key validator; onlyGET /openapi.jsonat the process root actually served the spec. Both URLs now serve the same document without authentication.
Deprecated
- None.
Breaking
- None in this entry.
/api/v1is 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
serverslists 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}/cancelwith requiredidempotency_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-Timestampis 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_idon hold/confirm/webhook bodies is ignored. The API key is the partner identity.- Hold creation does not require
partner_idin the body (it used to 400 even though the spec never required it). - Missing hold/booking ids return
404, not500.
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).
