API_CONTRACTS — bff-tenant-booking-service
Sibling: SERVICE_OVERVIEW · APPLICATION_LOGIC · DOMAIN_MODEL · SECURITY_MODEL
Cross-cutting: 05 API Design · Standards · ERROR_CODES · Standards · NAMING
1. Surface, base URL, conventions
- Base URL (prod):
https://{tenantSlug}.melmastoon.ghasi.io/apiorhttps://booking.<tenant-custom-domain>/api - Internal canonical:
https://api.melmastoon.ghasi.io/bff/tenant-booking/v1/{tenantSlug}/...(used by mobile + dev tools) - Surface: REST + JSON. Anonymous-first. No SSE. No WebSockets. Cloud CDN sits in front for read endpoints.
- Auth: anonymous (Phase 1). A
tnt_<ulid>cookie is minted on first response. Phase 2 introduces optionalAuthorization: Bearer <jwt>for guest sign-in. - Headers: request envelope per 05 §3. Errors per ERROR_CODES.
- Versioning: path-prefix major version (
/v1). Breaking shape changes require/v2. - OpenAPI: generated from controllers; published to
https://docs.melmastoon.ghasi.io/openapi/bff-tenant-booking.json. - Tenant scope: every route is implicitly scoped to
{tenantSlug}(resolved inTenantContextGuard); no path needs atenantIdparameter.
2. Common request headers
| Header | Required | Notes |
|---|---|---|
Cookie: tnt_id=<ulid> | first response sets; subsequent required | Anonymous tenant-booking session |
X-Request-Id | recommended | Generated by client; gateway fills if absent |
Accept-Language | recommended | BCP-47; resolves locale within tenant's supported set |
X-Currency | optional | ISO 4217; overrides session preference if in tenant's currencies[] |
X-Idempotency-Key | required on POST/PATCH mutating routes | ULID |
traceparent | auto | W3C trace context |
X-Client-Surface | recommended | web-tenant or mobile-tenant |
X-Bootstrap-Version | recommended | Composed-from version client holds; BFF returns 410 if unacceptably stale |
X-Handoff-Token | optional | Pass-through of consumer BFF mint; if present, BFF auto-consumes during bootstrap |
3. Common response headers
| Header | Notes |
|---|---|
Set-Cookie: tnt_id=<ulid>; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000; Path=/api | On bootstrap |
X-Request-Id | Echoed |
X-Cache: HIT / MISS / STALE / BYPASS | Cache disposition |
X-Tenant-Id | Resolved tenant ULID (for client telemetry) |
X-Bootstrap-Version | Composed-from version of the response (when applicable) |
Cache-Control | Per-route (see below) |
Vary | Accept-Language, X-Currency, Accept-Encoding, Cookie |
Content-Security-Policy | Generated per response with embedded nonce |
RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset | RFC 9239 |
traceparent | Echoed |
4. Endpoint catalogue
| Method | Path (after /bff/tenant-booking/v1/{tenantSlug}) | Purpose | Cache | Idempotent |
|---|---|---|---|---|
GET | /bootstrap | Tenant bootstrap (theme + flow + locales + currencies + payment + policies) | edge: 30 s; app: 5 min | yes |
GET | /policies | Tenant policies | edge: 5 min; app: 1 h | yes |
GET | /properties/{propertyId} | Property summary (rooms, photos, policies) | edge: 60 s; app: 5 min | yes |
GET | /properties/{propertyId}/rooms | Room types with photos | edge: 60 s; app: 5 min | yes |
GET | /availability | Per-room-type availability + cheapest rate | edge: 30 s; app: 30 s | yes |
POST | /quote | Issue PriceQuote | none | yes (Idem-Key) |
POST | /hold | Create hold + bdr_ draft | none | yes (Idem-Key) |
GET | /draft/{draftId} | Read current draft state | none | yes |
PATCH | /draft/{draftId} | Update guest details / preferences | none | yes (Idem-Key) |
POST | /draft/{draftId}/payment-intent | Create PaymentIntent + redirect URL | none | yes (Idem-Key) |
POST | /draft/{draftId}/return | Payment provider redirect-return; confirms reservation | none | yes (Idem-Key + replay-safe) |
POST | /draft/{draftId}/confirm | Confirm via non-redirect rails (cash, MFS push) | none | yes (Idem-Key) |
DELETE | /draft/{draftId} | Abandon draft early | none | yes |
GET | /confirmation/{reservationId} | Composed confirmation view | edge: 30 s; app: 5 min | yes |
POST | /handoff/consume | Verify + consume inbound handoff token | none | yes (replay-safe) |
POST | /internal/handoff/{id}/consume | Internal endpoint exposed to bff-consumer-service (mutual-TLS + HMAC) | none | yes |
GET | /session | Read sanitized session | none | yes |
POST | /session/locale | Set locale preference | none | yes |
POST | /session/currency | Set display currency | none | yes |
POST | /session/clear | Drop session blob (logout-equivalent) | none | yes |
GET | /health/live | Liveness | none | yes |
GET | /health/ready | Readiness (Memorystore + Postgres + upstream circuits) | none | yes |
5. GET /bootstrap
5.1 Request
GET /bff/tenant-booking/v1/kabul-grand-hotel/bootstrap HTTP/1.1
Host: kabul-grand-hotel.melmastoon.ghasi.io
Accept-Language: ps-AF, en;q=0.5
X-Currency: AFN
X-Bootstrap-Version: tv-2026-04-01,cv-12
X-Handoff-Token: hf_v1.eyJ0bnQ6Ii4uLg.SIG
5.2 Response (200)
{
"data": {
"tenantId": "tnt_01H8Y...",
"tenantSlug": "kabul-grand-hotel",
"brandName": "Kabul Grand Hotel",
"brandTagline": "د کابل ښکاره مهماني",
"theme": {
"locale": "ps-AF",
"isRtl": true,
"currency": "AFN",
"paletteRef": "thm_01H8Y...",
"designTokensCssUrl": "https://cdn.melmastoon.ghasi.io/themes/thm_01H8Y/v3.css",
"logoUrl": "https://cdn.melmastoon.ghasi.io/tenants/tnt_01H8Y/logo.svg",
"faviconUrl": "https://cdn.melmastoon.ghasi.io/tenants/tnt_01H8Y/favicon.ico",
"fontFamilies": ["Vazirmatn", "system-ui"],
"brandColors": { "primary": "#1F6F4A", "secondary": "#C2A04E", "accent": "#0E3B27", "surface": "#F8F4EC", "onSurface": "#1A1A1A" }
},
"flowConfig": {
"steps": ["searching","selecting","quoting","holding","collecting_details","paying","awaiting_return","confirming","confirmed"],
"optionalGuestFields": ["nationalIdNumber","arrivalTime"],
"paymentRailsOrder": ["card","paypal","mfs","cash_on_arrival"],
"cashOnArrivalEnabled": true,
"requireGuestSignIn": false
},
"locales": [
{ "tag": "ps-AF", "displayName": "پښتو", "isRtl": true },
{ "tag": "fa-AF", "displayName": "دری", "isRtl": true },
{ "tag": "en-US", "displayName": "English", "isRtl": false }
],
"currencies": [
{ "code": "AFN", "symbol": "؋", "decimalPlaces": 0, "isPrimary": true },
{ "code": "USD", "symbol": "$", "decimalPlaces": 2, "isPrimary": false }
],
"paymentMethods": [
{ "method": "card", "providers": ["adyen"], "displayName": { "ps-AF": "د کارت تادیه" } },
{ "method": "paypal", "providers": ["paypal"], "displayName": { "ps-AF": "PayPal" } },
{ "method": "mfs", "providers": ["hesb-pay","mpaisa"], "displayName": { "ps-AF": "د موبایل پيسې" } },
{ "method": "cash_on_arrival", "providers": ["onsite"], "displayName": { "ps-AF": "د راتګ پر مهال نقدې" } }
],
"policies": [
{ "kind": "cancellation", "ref": "pol_01H8Y...", "url": "https://...", "summary": "..." },
{ "kind": "privacy", "ref": "pol_01H8Y...", "url": "https://...", "summary": "..." }
],
"handoffPayload": {
"consumerSessionId": "gms_01H8Y...",
"tenantId": "tnt_01H8Y...",
"propertyId": "ppt_01H8Y...",
"checkIn": "2026-05-20",
"checkOut": "2026-05-22",
"occupancy": { "adults": 2, "children": 0, "rooms": 1 },
"currency": "AFN",
"locale": "ps-AF",
"campaign": { "source": "google", "medium": "cpc", "campaign": "spring-2026" },
"expiresAt": "2026-04-23T10:14:22.041Z"
},
"serverTime": "2026-04-23T09:44:22.041Z",
"csp": { "nonce": "Z4V8b6...A==" },
"composedAt": "2026-04-23T09:44:22.041Z",
"composedFrom": { "themeVersion": "thm_01H8Y_v3", "tenantConfigVersion": "v12" }
},
"meta": { "cache": "MISS", "fanoutLatencyMs": { "tenant": 14, "theme": 38, "policies": 22 } }
}
5.3 Errors
| Status | Code |
|---|---|
| 404 | MELMASTOON.BFF.TENANT.SLUG_UNKNOWN |
| 503 | MELMASTOON.TENANT.SUSPENDED |
| 410 | MELMASTOON.BFF.TENANT.BOOTSTRAP_STALE (when X-Bootstrap-Version is older than 7 days) |
| 502 | MELMASTOON.BFF.UPSTREAM_UNAVAILABLE (when both tenant-service AND no cache available) |
6. GET /availability
6.1 Request
GET /bff/tenant-booking/v1/kabul-grand-hotel/availability?propertyId=ppt_01H8Y...&checkIn=2026-05-20&checkOut=2026-05-22&adults=2&children=0&rooms=1¤cy=AFN HTTP/1.1
Cookie: tnt_id=tnt_01H8Y...
6.2 Response (200)
{
"data": {
"stayWindow": { "checkIn": "2026-05-20", "checkOut": "2026-05-22", "nights": 2 },
"currency": "AFN",
"rooms": [
{
"roomTypeId": "rmt_01H8Y...",
"roomTypeName": { "ps-AF": "د دوه کسانو خونه", "en-US": "Double Room" },
"photos": ["https://cdn.../1.jpg","https://cdn.../2.jpg"],
"amenities": ["wifi","ac","breakfast"],
"maxOccupancy": { "adults": 2, "children": 1 },
"available": true,
"remainingUnits": 3,
"ratePlans": [
{
"ratePlanId": "rate_01H8Y...",
"displayName": { "ps-AF": "ارزانه ډرف" },
"refundability": "non_refundable",
"totalDisplay": { "currency": "AFN", "amountMinor": 12500, "formatted": "AFN 12,500" },
"perNightDisplay": { "currency": "AFN", "amountMinor": 6250, "formatted": "AFN 6,250" }
}
]
}
]
},
"meta": { "cache": "MISS", "stale": false, "partial": false }
}
6.3 Errors
| Status | Code |
|---|---|
| 422 | MELMASTOON.GENERAL.VALIDATION_FAILED |
| 422 | MELMASTOON.INVENTORY.MIN_LOS_VIOLATED / MAX_LOS_VIOLATED |
| 502 | MELMASTOON.BFF.UPSTREAM_UNAVAILABLE |
| 504 | MELMASTOON.BFF.UPSTREAM_TIMEOUT |
7. POST /quote
7.1 Request
POST /bff/tenant-booking/v1/kabul-grand-hotel/quote HTTP/1.1
Cookie: tnt_id=tnt_01H8Y...
X-Idempotency-Key: req_01H8YN7QF9RTBRZG4F8Y5CK4MV
Content-Type: application/json
{
"propertyId": "ppt_01H8Y...",
"roomTypeId": "rmt_01H8Y...",
"ratePlanId": "rate_01H8Y...",
"checkIn": "2026-05-20",
"checkOut": "2026-05-22",
"occupancy": { "adults": 2, "children": 0, "rooms": 1 },
"promoCode": "SPRING25",
"currency": "AFN"
}
7.2 Response (201)
{
"data": {
"quoteId": "qte_01H8Y...",
"expiresAt": "2026-04-23T10:14:22.041Z",
"totalDisplay": { "currency": "AFN", "amountMinor": 12500, "formatted": "AFN 12,500" },
"totalBase": { "currency": "AFN", "amountMinor": 12500 },
"lineItems": [
{ "kind": "room", "perNightDisplay": { "currency":"AFN","amountMinor":6250,"formatted":"AFN 6,250" }, "nights": 2 },
{ "kind": "tax", "displayName": { "ps-AF": "د سیلانیانو مالیه" }, "amountDisplay": { "currency":"AFN","amountMinor":625,"formatted":"AFN 625" } },
{ "kind": "discount", "displayName": { "ps-AF": "د پسرلي تخفیف" }, "amountDisplay": { "currency":"AFN","amountMinor":-1250,"formatted":"AFN -1,250" } }
],
"policies": { "cancellation": "non_refundable" }
}
}
7.3 Errors
| Status | Code |
|---|---|
| 404 | MELMASTOON.PRICING.RATE_PLAN_NOT_FOUND |
| 409 | MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY |
| 422 | MELMASTOON.PRICING.CURRENCY_MISMATCH |
| 502 | MELMASTOON.BFF.UPSTREAM_UNAVAILABLE |
8. POST /hold
8.1 Request
POST /bff/tenant-booking/v1/kabul-grand-hotel/hold HTTP/1.1
Cookie: tnt_id=tnt_01H8Y...
X-Idempotency-Key: req_01H8YN7QHA9RTBRZG4F8Y5CK4MV
Content-Type: application/json
{
"quoteId": "qte_01H8Y...",
"occupancy": { "adults": 2, "children": 0, "rooms": 1 },
"currency": "AFN",
"searchParams": { "checkIn": "2026-05-20", "checkOut": "2026-05-22", "promoCode": "SPRING25" }
}
8.2 Response (201)
{
"data": {
"draftId": "bdr_01H8Y...",
"reservationId": "rsv_01H8Y...",
"holdExpiresAt": "2026-04-23T10:14:22.041Z",
"totalDisplay": { "currency": "AFN", "amountMinor": 12500, "formatted": "AFN 12,500" },
"flowState": "collecting_details"
}
}
8.3 Errors
| Status | Code |
|---|---|
| 410 | MELMASTOON.PRICING.QUOTE_EXPIRED |
| 409 | MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY |
| 409 | MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED |
| 502 | MELMASTOON.BFF.UPSTREAM_UNAVAILABLE |
9. PATCH /draft/{draftId}
Updates guest details and optional preferences. Idempotent under X-Idempotency-Key.
{
"guest": {
"primaryGuest": {
"firstName": "اسماء",
"lastName": "احمدی",
"email": "asma@example.com",
"phone": "+93701234567",
"nationalIdNumber": "AFG-...",
"preferredLocale": "ps-AF"
},
"additionalGuests": [],
"specialRequests": "د مهربانۍ په اړه د راڅرګندیدلو وخت 22:00 دی"
},
"expectedUpdatedAt": "2026-04-23T09:50:11.000Z"
}
Response (200) returns the updated draft. expectedUpdatedAt mismatch → 412 MELMASTOON.BFF.TENANT.DRAFT_CONFLICT.
10. POST /draft/{draftId}/payment-intent
{
"method": "card",
"provider": "adyen",
"savePaymentMethod": false,
"billingAddress": { "country": "AF", "city": "Kabul", "postalCode": "1001" }
}
10.1 Response (201)
{
"data": {
"intentId": "pyi_01H8Y...",
"redirectUrl": "https://checkoutshopper-test.adyen.com/checkout/...&token=...",
"method": "card",
"provider": "adyen",
"expiresAt": "2026-04-23T10:14:22.041Z"
}
}
For cash_on_arrival no redirectUrl is returned; the flowState advances directly to confirming after POST /confirm.
10.2 Errors
| Status | Code |
|---|---|
| 502 | MELMASTOON.PAYMENT.GATEWAY_TIMEOUT |
| 422 | MELMASTOON.PAYMENT.INTENT_NOT_FOUND (provider rejected) |
| 422 | MELMASTOON.GENERAL.VALIDATION_FAILED |
11. POST /draft/{draftId}/return
Called by the SPA after the redirect-back from the payment provider. Body carries the provider's return-state.
{
"returnState": "redirectResult=eyJ0eXAiOiJKV1Q...",
"providerReference": "ADYEN-PSP-REF-1234"
}
11.1 Response (200) — successful confirm
{
"data": {
"kind": "confirmed",
"reservationId": "rsv_01H8Y...",
"flowState": "confirmed",
"redirectTo": "/booking/confirmation/rsv_01H8Y..."
}
}
11.2 Response (200) — already confirmed (idempotent re-entry)
{
"data": {
"kind": "already_confirmed",
"reservationId": "rsv_01H8Y...",
"flowState": "confirmed",
"redirectTo": "/booking/confirmation/rsv_01H8Y..."
}
}
11.3 Errors
| Status | Code |
|---|---|
| 402 | MELMASTOON.PAYMENT.DECLINED |
| 502 | MELMASTOON.BFF.TENANT.PAYMENT_RETURN_INVALID |
| 504 | MELMASTOON.PAYMENT.GATEWAY_TIMEOUT |
| 409 | MELMASTOON.RESERVATION.HOLD_EXPIRED |
| 409 | MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED |
12. GET /confirmation/{reservationId}
{
"data": {
"reservation": {
"reservationId": "rsv_01H8Y...",
"status": "confirmed",
"checkIn": "2026-05-20",
"checkOut": "2026-05-22",
"roomType": { "id": "rmt_01H8Y...", "name": { "ps-AF": "د دوه کسانو خونه" } }
},
"folio": {
"folioId": "fol_01H8Y...",
"totalDisplay": { "currency": "AFN", "amountMinor": 12500, "formatted": "AFN 12,500" },
"balanceDisplay": { "currency": "AFN", "amountMinor": 0, "formatted": "AFN 0" }
},
"guest": { "firstName": "اسماء", "lastName": "احمدی", "preferredLocale": "ps-AF" },
"property": { "propertyId": "ppt_01H8Y...", "name": { "ps-AF": "..."}, "address": { "ps-AF": "..." } },
"keyCredentialPlaceholder": { "kind": "qr_pending", "issuesAt": "2026-05-20T10:00:00Z" },
"postStay": { "checkInInstructions": { "ps-AF": "..." }, "wifiCode": null },
"support": { "phone": "+93202345678", "email": "support@kabulgrand.af", "whatsapp": "+93701234567" }
},
"meta": { "cache": "HIT" }
}
13. POST /handoff/consume
Called by the SPA when navigating directly to a booking page with a handoff token in the URL.
{ "token": "hf_v1.eyJ0bnQ6Ii4uLg.SIG" }
Response (200):
{
"data": {
"handoffArrivalId": "bha_01H8Y...",
"payload": {
"tenantId": "tnt_01H8Y...",
"propertyId": "ppt_01H8Y...",
"checkIn": "2026-05-20",
"checkOut": "2026-05-22",
"occupancy": { "adults": 2, "children": 0, "rooms": 1 },
"currency": "AFN",
"locale": "ps-AF",
"campaign": { "source": "google", "medium": "cpc", "campaign": "spring-2026" }
}
}
}
Errors:
| Status | Code |
|---|---|
| 401 | MELMASTOON.BFF.TENANT.HANDOFF_SIGNATURE_INVALID |
| 410 | MELMASTOON.BFF.TENANT.HANDOFF_EXPIRED |
| 409 | MELMASTOON.BFF.TENANT.HANDOFF_REPLAYED |
| 422 | MELMASTOON.BFF.TENANT.HANDOFF_TENANT_MISMATCH |
14. POST /internal/handoff/{id}/consume
Internal endpoint reachable only from bff-consumer-service over mutual-TLS within the VPC. Used when the consumer BFF needs to mark its mint as consumed by us.
POST /bff/tenant-booking/v1/internal/handoff/bhd_01H8Y.../consume HTTP/1.1
Authorization: Bearer <google-id-token>
X-Internal-HMAC: <hex>
Response (200): { "data": { "consumed": true, "consumedAt": "2026-04-23T..." } }. Replay → { "data": { "consumed": true, "alreadyConsumed": true } }.
15. Rate limits
| Bucket | Limit |
|---|---|
| Per-tenant per-IP per-second | 50 |
| Per-tenant per-IP per-minute | 600 |
| Per-tenant global per-second | 200 (default; overridable per-tenant) |
/quote per-session per-minute | 30 |
/hold per-session per-hour | 20 |
/payment-intent per-session per-hour | 6 |
/return per-session per-hour | 30 (idempotent re-entry tolerated) |
Returns 429 + MELMASTOON.GENERAL.RATE_LIMITED with Retry-After.
16. Webhooks
This BFF does not receive webhooks. Payment-provider webhooks land at payment-gateway-service; their effects propagate via events the BFF doesn't directly subscribe to (it polls payment-gateway-service on /return).
17. CORS
Allow-list per environment:
- prod:
https://*.melmastoon.ghasi.io,https://booking.<verified-tenant-domain>(per-tenant entries managed in Terraform). - staging:
https://*.staging.melmastoon.ghasi.io. - dev:
http://localhost:3000,http://*.local.melmastoon.test:3000.
Access-Control-Allow-Credentials: true. Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS. Access-Control-Allow-Headers enumerates the headers in §2.
18. Pagination
This BFF returns small composed view-models; only /properties/{propertyId}/rooms paginates (rare; most tenants have < 50 room types):
- Cursor-based:
?after=<cursor>&limit=<1..50>. - Response:
{ "data": [...], "page": { "cursor": "...", "hasMore": true } }.
19. Cache directives
| Endpoint | Cache-Control |
|---|---|
/bootstrap | private, max-age=30, stale-while-revalidate=60 |
/policies | public, max-age=300, stale-while-revalidate=600 |
/availability | private, max-age=15, stale-while-revalidate=30 |
/properties/... | public, max-age=60, stale-while-revalidate=300 |
/quote /hold /draft/... /return /confirm | no-store |
/confirmation/{id} | private, max-age=30, stale-while-revalidate=60 |
/session* | no-store |
20. Backwards compatibility
Breaking changes require /v2. Non-breaking additions (new fields) ship under /v1 with X-API-Notice deprecation headers when older fields will be removed.