Booking Service API Changes - v1.65.0

Release: v1.65.0 Date: 2026-04-29


Summary

Aspect Status
Breaking changes NO
New endpoints 1 booking endpoint (/directsale), 2 admin endpoint groups (federation instances, library audit)
Modified endpoints 0 (V1 endpoints gain a booking_disabled enforcement check, but the contract is unchanged for enabled orgs)
Removed endpoints 0
Auth changes NO

New Endpoints

POST /api/private/v2.0/booking/directsale?eventId={uuid}

Issue: SEAT-957

Atomic ACTIVE → SOLD transition for seats and general-admission capacity. Bypasses the LOCKED intermediate state that /sale requires. Use for non-interactive flows (back-office sales, comp tickets, imports).

Auth: same as /sale@securityServiceV2.hasAccessToEvent(eventId). Caller’s organization must have access to the event; the V2 enforcement of booking_disabled is honored.

Request

POST /api/private/v2.0/booking/directsale?eventId=8d4f0f3a-2b8e-4d2a-9c8e-1d3a2b8e4d2a
Content-Type: application/json
Authorization: Bearer {token}

{
  "sessionId": "abc123",
  "seats": [
    { "id": 42 },
    { "id": 43 }
  ],
  "groupOfSeats": [
    { "id": 7, "capacity": 4 }
  ],
  "holdType": "DIRECT"
}

Body: same StateSelection shape as /sale. holdType is optional and informational on the audit row only — direct sale does not transition through a hold.

Response

  • 200 true — all seats and GA capacity transitioned successfully.
  • 200 false — at least one seat is not in ACTIVE state, or at least one GA activeCount is below the requested capacity. Operation is atomic per request — partial application is rejected.
  • 402 ORGANIZATION_DISABLED — caller’s organization has booking_disabled=true.
  • 403 Forbidden — caller does not have access to the event.

curl example

curl -X POST \
  "https://api.seatmap.dev/api/private/v2.0/booking/directsale?eventId=8d4f0f3a-2b8e-4d2a-9c8e-1d3a2b8e4d2a" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
        "sessionId": "abc123",
        "seats": [{"id": 42}, {"id": 43}]
      }'
# 200
# true

Behaviour

  • Required source state is ACTIVE. LOCKED, SOLD, and BLOCKED seats are rejected (returns false, no state change).
  • GA capacity is deducted from activeCount directly — no intermediate locked bucket.
  • Each successful transition writes a row to seat_state_events with old_state=ACTIVE, new_state=SOLD so the outbox and downstream analytics observe the source state correctly.
  • The existing /sale endpoint and its semantics are unchanged. /directsale is opt-in.

When to use which

  • /sale — interactive checkout flows where a customer holds seats during payment. Continue with /lock then /sale.
  • /directsale — non-interactive flows (back-office sales, comp tickets, imports). One call, no holding state.

GET / POST / DELETE /api/admin/federation/instances

Issue: SEAT-952

Global-admin endpoints to manage federation API keys. Used by source-instance operators to issue keys to consumer instances. Plaintext keys are returned once on creation and never readable again.

Method Path Description
GET /api/admin/federation/instances Paginated list (default 50, sort by createdAt DESC). Returns label, hash prefix, status.
POST /api/admin/federation/instances Issue a new key. Returns the plaintext key in the response — store it immediately.
POST /api/admin/federation/instances/{id}/revoke Revoke. Sets revoked_at. Existing requests under the key fail thereafter.

Auth: ROLE_GLOBAL_ADMIN.

Create key — request

{
  "label": "Timepad on-prem",
  "rateLimitRpm": 60
}

Create key — response

{
  "id": 7,
  "label": "Timepad on-prem",
  "key": "fed_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "createdAt": "2026-04-29T12:34:56Z",
  "rateLimitRpm": 60
}

The plaintext key is shown in this response only. Subsequent reads return only the hash prefix and metadata.


GET /api/admin/library/audit/...

Issue: SEAT-948

Global-admin audit viewer for library copy operations. Backed by the library_pull_log table introduced in V104.

Method Path Description
GET /api/admin/library/audit Paginated audit log. Filters: venueId, organizationId, instanceKeyHash.
GET /api/admin/library/audit/{id} Single audit row.

Auth: ROLE_GLOBAL_ADMIN.

The audit captures both in-instance copies and federation pulls; rows triggered by federation carry instance_api_key_hash, in-instance copies carry user_id and organization_id.


Behavioral Changes

V1 booking endpoints — booking_disabled parity

Issue: SEAT-959

The legacy V1 booking API now honors organization.booking_disabled (introduced in 1.63.0 by SEAT-928). V1 paths previously bypassed the flag, so a disabled organization could still POST to /api/private/v1.0/booking/{lock,unlock,sale}.

Affected endpoints (all return 402 when the org is disabled):

  • POST /api/private/v1.0/booking/lock
  • POST /api/private/v1.0/booking/unlock
  • POST /api/private/v1.0/booking/sale
  • GET /api/private/v1.0/venues/...
  • GET /api/private/v1.0/events/...
  • GET /api/private/v1.0/prices/...

Response shape (matches V2):

HTTP/1.1 402 Payment Required
Content-Type: application/json

{
  "error": "ORGANIZATION_DISABLED",
  "message": "Organization is disabled for booking"
}

Migration: integrators must surface 402 responses on the V1 path the same way they handle 4xx errors. Active organizations are unaffected — the change is transparent for them.


Removed Endpoints

No removed endpoints in this release.


Auth / Token Changes

No public auth contract changes.

The editor-service introduces an internal JWT shape change for impersonation (impersonator and impersonation_session_id claims), but this is editor-only and does not surface in booking-service.


OpenAPI / Swagger

The booking-service Swagger surface (/swagger-ui/index.html) reflects the new /directsale endpoint with full schemas. The admin federation and library audit endpoints surface in the editor-service Swagger under the Federation instances and Library audit tags.


Migration Guide

  1. Existing /sale callers: no changes required. /directsale is a separate endpoint; existing flows continue to use /sale with /lock first.
  2. V1 booking integrators: ensure 402 handling is in place. If the integration’s response handler treated all 4xx as transient, check that 402 routes to an operator-visible alert (the org has been disabled).
  3. Self-hosted operators consuming federation: see deployment.md for the Helm + Secret setup. The source-instance admin issues your key once via POST /api/admin/federation/instances.

References