Skip to main content

API_CONTRACTS — reservation-service

Sibling: APPLICATION_LOGIC · DOMAIN_MODEL · SECURITY_MODEL · EVENT_SCHEMAS

Strategic anchors: 05 API Design · standards/NAMING · standards/ERROR_CODES

All endpoints are versioned at /api/v1/reservations. Request/response bodies are JSON; errors are RFC 7807 Problem+JSON. Money is serialized as { "amountMicro": "12500000", "currency": "USD" } (string for bigint over JSON). Timestamps are ISO-8601 UTC unless explicitly noted as a property-local date (YYYY-MM-DD). All mutating endpoints accept Idempotency-Key and propagate traceparent.

This service is fronted by bff-tenant-booking-service (guest funnel) and bff-backoffice-service (front-desk staff). Direct client traffic is not allowed; the BFFs hold the only outbound IAM principals authorized to call us.


1. Common request headers

HeaderRequiredNotes
Authorization: Bearer <jwt>yesIssued by iam-service for staff; or anonymous booking session token for guests
X-Tenant-Id: tnt_…yesCross-checked against JWT; mismatch → 403 MELMASTOON.TENANT.MISMATCH
X-Property-Id: ppt_…for staff endpointsValidated against staff property access
Idempotency-Key: <ULID>yes for POST/PATCH24h dedupe window
Accept-Language: <bcp47>recommendedDrives Problem+JSON title localization
traceparent: 00-…-…-01yes (set by gateway)W3C trace propagation

2. Common error envelope (RFC 7807 + extensions)

{
"type": "https://errors.melmastoon.com/RESERVATION/OVERBOOKING_BLOCKED",
"title": "Selected room is no longer available for the chosen dates.",
"status": 409,
"code": "MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED",
"detail": "Inventory rejected the hold for room rmu_01H3Z… on 2026-04-25..2026-04-27.",
"instance": "/api/v1/reservations/holds",
"traceId": "01H3Z4WK7…",
"tenantId": "tnt_01H…",
"violations": [
{ "field": "items[0].roomTypeId", "rule": "no_inventory" }
]
}

Common status mapping:

StatusUsed for
400validation, malformed request
401missing/invalid JWT
403tenant mismatch, RBAC/ABAC denial
404not found (or RLS-hidden)
409OCC, illegal transition, overbooking, hold limit, saga in flight
410quote/hold expired
422domain invariant violation that isn't a transient conflict
429rate limit
500unhandled
503downstream (pricing, inventory, payment, lock) unavailable

3. Endpoints

3.1 POST /api/v1/reservations/quotes

Request a price quote with TTL.

Request

{
"propertyId": "ppt_01H3Z3GXSE7Y8B5RBQ8H6JQK7Z",
"stayWindow": { "start": "2026-04-25", "end": "2026-04-28" },
"items": [
{ "roomTypeId": "rmt_01H3Z…SUITE", "occupants": { "adults": 2, "children": 1, "infants": 0 } },
{ "roomTypeId": "rmt_01H3Z…TWIN", "occupants": { "adults": 2, "children": 0, "infants": 0 } }
],
"ratePlanIdHint": "rate_01H…BAR",
"channel": "direct",
"locale": "ps-AF",
"promoCode": "WELCOME10"
}

Response 200

{
"quoteId": "qte_01H3Z…",
"ttlSeconds": 900,
"expiresAt": "2026-04-22T18:46:00Z",
"items": [
{
"roomTypeId": "rmt_01H3Z…SUITE",
"nightlyBreakdown": [
{ "date": "2026-04-25", "rate": { "amountMicro": "120000000", "currency": "USD" }, "rateCode": "BAR", "taxMicro": "12000000" },
{ "date": "2026-04-26", "rate": { "amountMicro": "120000000", "currency": "USD" }, "rateCode": "BAR", "taxMicro": "12000000" },
{ "date": "2026-04-27", "rate": { "amountMicro": "140000000", "currency": "USD" }, "rateCode": "BAR", "taxMicro": "14000000" }
],
"itemSubtotal": { "amountMicro": "380000000", "currency": "USD" }
}
],
"totals": {
"subtotal": { "amountMicro": "560000000", "currency": "USD" },
"discountTotal": { "amountMicro": "56000000", "currency": "USD" },
"taxTotal": { "amountMicro": "55000000", "currency": "USD" },
"grandTotal": { "amountMicro": "559000000", "currency": "USD" },
"inTenantCurrency": { "amountMicro": "23478000000000", "currency": "IRR" }
},
"fxSnapshot": {
"base": "USD", "quote": "IRR", "rate": 42000.0,
"source": "pricing_service", "capturedAt": "2026-04-22T18:31:00Z"
}
}

Errors: 400 validation, 404 PROPERTY_NOT_FOUND, 503 PRICING_UNAVAILABLE, 503 INVENTORY_UNAVAILABLE.

3.2 POST /api/v1/reservations/holds

Place a 10-minute hold and create the payment intent.

Request

{
"quoteId": "qte_01H3Z…",
"primaryGuest": {
"fullName": { "given": "اسماعیل", "family": "خان", "scriptHint": "pashto" },
"email": "ismail.khan@example.af",
"phoneE164": "+93700000000",
"locale": "ps-AF",
"documentRef": { "type": "national_id", "numberLast4": "8421" }
},
"additionalGuests": [
{ "fullName": { "given": "Sadia", "family": "Khan", "scriptHint": "latin" }, "ageBand": "adult" },
{ "fullName": { "given": "Yusuf", "family": "Khan", "scriptHint": "latin" }, "ageBand": "child", "isChildWithoutId": true }
],
"specialRequests": [
{ "freeText": "د حلال خواړو غوښتنه؛ غونډلې کوټه که شونې وي.", "locale": "ps-AF" }
],
"paymentMethodHint": "card"
}

Response 201

{
"reservation": {
"id": "rsv_01H3Z4WK7TS…",
"reservationCode": "GM-9F4K2C",
"status": "held",
"holdExpiresAt": "2026-04-22T18:41:00Z",
"stayWindow": { "start": "2026-04-25", "end": "2026-04-28", "nights": 3 },
"totals": { "grandTotal": { "amountMicro": "559000000", "currency": "USD" } },
"channel": { "channel": "direct", "capturedAt": "2026-04-22T18:31:00Z" }
},
"paymentIntent": {
"paymentIntentId": "pyi_01H3Z…",
"clientSecret": "ps_live_…opaque…",
"provider": "card",
"amount": { "amountMicro": "559000000", "currency": "USD" }
}
}

Errors: 410 QUOTE_EXPIRED, 409 OVERBOOKING_BLOCKED, 409 HOLD_LIMIT_EXCEEDED, 503 PAYMENT_GATEWAY_UNAVAILABLE.

3.3 POST /api/v1/reservations

Create + confirm in one shot (used by walk-in flow that has already captured payment, or by cash-on-arrival paths). For the standard flow, use holds then wait for the payment webhook.

Request

{
"quoteId": "qte_01H3Z…",
"primaryGuest": { "...": "..." },
"paymentMethod": "cash_on_arrival",
"channel": "walk_in",
"actorStaffId": "stf_01H…"
}

Response 201 — same shape as GET /:id.

3.4 GET /api/v1/reservations/:reservationId

Read a reservation. RLS scopes to tenant; 404 if not visible.

Response 200 — full Reservation (DOMAIN_MODEL §2). Money fields stringified, dates property-local where stay-related, UTC otherwise.

3.5 PATCH /api/v1/reservations/:reservationId

Modify. The body must include a type discriminator from §2.5 of APPLICATION_LOGIC.

Request — date_change

{
"type": "date_change",
"newStayWindow": { "start": "2026-04-26", "end": "2026-04-30" },
"actorStaffId": "stf_01H…",
"reason": "Guest extended trip"
}

Request — room_change

{
"type": "room_change",
"items": [
{ "itemId": "01H3Z…", "newRoomTypeId": "rmt_…SUITE_PLUS" }
],
"actorStaffId": "stf_01H…"
}

Request — special_request_added

{
"type": "special_request_added",
"request": { "freeText": "Late check-in around 02:00 local", "locale": "en-US" }
}

Response 200 — updated Reservation.

Errors: 409 ILLEGAL_TRANSITION, 409 SAGA_IN_FLIGHT, 409 STALE_VERSION, 422 RETROACTIVE_DATE_CHANGE, 403 POLICY_VERSION_MISMATCH, 503 PRICING_UNAVAILABLE, 503 INVENTORY_UNAVAILABLE.

3.6 POST /api/v1/reservations/:reservationId/cancel

Request

{
"reason": "Guest changed plans",
"actorStaffId": "stf_01H…",
"policyOverride": null
}

Response 200

{
"reservation": { "id": "rsv_…", "status": "cancelled", "version": 7 },
"policyApplied": "tenant.cancellation_policy@v3",
"refundEligibility": { "amountMicro": "279500000", "currency": "USD" },
"penalty": { "amountMicro": "279500000", "currency": "USD" }
}

3.7 POST /api/v1/reservations/:reservationId/check-in

Request

{
"actorStaffId": "stf_01H…",
"earlyOverride": false,
"documentScans": [
{ "guestRef": "primary", "fileMediaId": "med_01H…" }
],
"issueKey": true
}

Response 200

{
"reservation": { "id": "rsv_…", "status": "checked_in", "version": 9 },
"key": { "credentialId": "key_01H…", "issuedAt": "2026-04-25T11:02:14Z", "deliveryChannels": ["sms","desktop"] },
"folio": { "folioId": "fol_01H…", "openedAt": "2026-04-25T11:02:11Z" }
}

Errors: 409 CHECK_IN_TOO_EARLY, 409 ROOM_NOT_READY, 503 LOCK_VENDOR_UNREACHABLE (response succeeds with requiresManualKey=true; not a 503).

3.8 POST /api/v1/reservations/:reservationId/check-out

Request

{
"actorStaffId": "stf_01H…",
"early": false,
"settleFolio": true
}

Response 200

{
"reservation": { "id": "rsv_…", "status": "checked_out", "version": 12 },
"folio": { "folioId": "fol_01H…", "balance": { "amountMicro": "0", "currency": "USD" }, "settledAt": "2026-04-28T10:14:00Z" },
"key": { "credentialId": "key_01H…", "revokedAt": "2026-04-28T10:14:01Z" }
}

3.9 POST /api/v1/reservations/:reservationId/walk-in

Combined create + check-in for a walk-in guest. Only callable by bff-backoffice-service.

Request

{
"propertyId": "ppt_01H…",
"stayWindow": { "start": "2026-04-22", "end": "2026-04-23" },
"items": [
{ "roomTypeId": "rmt_…TWIN", "occupants": { "adults": 2, "children": 0, "infants": 0 }, "preferredRoomId": "rmu_01H…105" }
],
"primaryGuest": {
"fullName": { "given": "Ali", "family": "Rahimi", "scriptHint": "latin" },
"phoneE164": "+93700000001",
"locale": "fa-AF"
},
"paymentMethod": "cash_on_arrival",
"channel": "walk_in",
"actorStaffId": "stf_01H…",
"deferKyc": false,
"issueKey": true
}

Response 201 — full reservation in checked_in state.

3.10 POST /api/v1/reservations/:reservationId/no-show

Request { "actorStaffId": "stf_…", "applyPolicy": true } Response 200 — reservation in no_show, with penalty summary and emitted events.

3.11 POST /api/v1/reservations/:reservationId/special-requests

Request

{ "freeText": "وعن الإفطار: حلال فقط، شكرا.", "locale": "ar-SA" }

Response 201

{
"request": {
"id": "01H…",
"tags": ["halal", "other"],
"freeText": "…",
"locale": "ar-SA",
"source": "ai_parser"
}
}

3.12 GET /api/v1/reservations/by-guest/:guestId

Query: ?includeFuture=true&includeCancelled=false&page=1&pageSize=20 Response 200 — paginated cursor list with items[] and nextCursor.

3.13 POST /api/v1/reservations/groups/:groupId/split

Split a group reservation into independent ones.

Request

{
"splits": [
{ "items": ["item-1", "item-2"], "primaryGuest": { "...": "..." } },
{ "items": ["item-3"], "primaryGuest": { "...": "..." } }
],
"actorStaffId": "stf_01H…"
}

Response 200 — array of new reservation summaries plus the cancellation summary of the original.

3.14 POST /api/v1/reservations/merge

Request

{
"survivingReservationId": "rsv_A",
"mergedReservationId": "rsv_B",
"actorStaffId": "stf_01H…"
}

Response 200 — the surviving reservation in its updated form.


4. Internal endpoints (network-internal only; not via Kong)

These are reachable only from the VPC; Cloud Run service-to-service auth enforced.

MethodPathPurpose
POST/internal/events/payment.transaction.captured.v1Pub/Sub push handler
POST/internal/events/payment.transaction.failed.v1Pub/Sub push handler
POST/internal/events/payment.transaction.refunded.v1Pub/Sub push handler
POST/internal/events/inventory.allocation.committed.v1Pub/Sub push handler
POST/internal/events/inventory.allocation.failed.v1Pub/Sub push handler
POST/internal/events/lock.key.issued.v1Pub/Sub push handler
POST/internal/events/lock.key.failed.v1Pub/Sub push handler
POST/internal/events/tenant.settings.changed.v1Pub/Sub push handler (refresh hold-window default)
POST/internal/events/property.room.taken_out_of_order.v1Pub/Sub push handler (re-accommodation flow)
POST/internal/jobs/expire-holdsCloud Scheduler invocation (every 30 s)
GET/internal/healthliveness + DB ping + Pub/Sub publisher health
GET/internal/readyoutbox relay lag + DB connection pool health

5. Pagination, sorting, filtering

  • All list endpoints use cursor-based pagination: ?cursor=<opaque>&pageSize=20 (max 100).
  • Default sort is createdAt desc; GET /by-guest/:guestId sorts by stayWindow.start desc.
  • Filtering on GET /by-guest: ?status=confirmed,checked_in&channel=direct,walk_in&dateFrom=…&dateTo=….

6. Versioning & deprecation

  • New optional fields may be added within v1 without notice.
  • Field removals, type changes, or required-field additions ship a new v2 resource group; both run in parallel for ≥ 8 weeks; Deprecation: <ISO> and Sunset: <ISO> response headers warn callers.

7. Sample sequences

7.1 Happy path (direct booking, card)

1. POST /quotes → quoteId
2. POST /holds → reservation(held), paymentIntent.clientSecret
3. <client uses Stripe.js to confirm payment>
4. payment-gateway-service publishes payment.transaction.captured.v1
5. reservation-service ConfirmReservationUseCase fires → reservation.confirmed.v1
6. notification-service sends confirmation; lock-integration deferred until check-in
7. (At check-in time)
POST /:id/check-in → reservation(checked_in), key, folio
8. (At check-out time)
POST /:id/check-out → reservation(checked_out), folio settled, key revoked

7.2 Walk-in cash-on-arrival

1. POST /:id/walk-in (operator, idempotency-key per session)
Internally: quote → hold → cash_on_arrival confirm → check-in → key issue
2. Operator records cash via billing-service `POST /folios/:id/payments` later
3. payment-gateway-service publishes payment.transaction.captured.v1 (cash event)
4. reservation-service no state change (already confirmed); folio reflects payment

7.3 Modify dates with delta charge

1. PATCH /:id body { type: "date_change", newStayWindow: {…} }
2. Internal: re-quote → reallocate → delta = +1 night
3. payment-gateway-service charges delta via existing payment method (or new intent)
4. lock-integration-service updates key validity
5. Response: reservation with new stayWindow + modification audit entry

7.4 Cancel with partial refund

1. POST /:id/cancel body { reason: "guest_cancelled" }
2. Policy: 50% refund; reservation.cancelled.v1 emitted
3. inventory-service releases allocation
4. payment-gateway-service refunds half
5. lock-integration-service revokes key (no-op if not yet issued)
6. notification-service sends cancellation notice in guest locale

7.5 Group partial cancel

1. PATCH /:id body { type: "room_removed", itemId: "01H…" }
2. inventory-service releases that allocation
3. delta refund issued
4. Reservation.status remains 'confirmed'; modification audit row added
5. If all items removed, status flips to 'cancelled'