Skip to main content

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/api or https://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 optional Authorization: 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 in TenantContextGuard); no path needs a tenantId parameter.

2. Common request headers

HeaderRequiredNotes
Cookie: tnt_id=<ulid>first response sets; subsequent requiredAnonymous tenant-booking session
X-Request-IdrecommendedGenerated by client; gateway fills if absent
Accept-LanguagerecommendedBCP-47; resolves locale within tenant's supported set
X-CurrencyoptionalISO 4217; overrides session preference if in tenant's currencies[]
X-Idempotency-Keyrequired on POST/PATCH mutating routesULID
traceparentautoW3C trace context
X-Client-Surfacerecommendedweb-tenant or mobile-tenant
X-Bootstrap-VersionrecommendedComposed-from version client holds; BFF returns 410 if unacceptably stale
X-Handoff-TokenoptionalPass-through of consumer BFF mint; if present, BFF auto-consumes during bootstrap

3. Common response headers

HeaderNotes
Set-Cookie: tnt_id=<ulid>; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000; Path=/apiOn bootstrap
X-Request-IdEchoed
X-Cache: HIT / MISS / STALE / BYPASSCache disposition
X-Tenant-IdResolved tenant ULID (for client telemetry)
X-Bootstrap-VersionComposed-from version of the response (when applicable)
Cache-ControlPer-route (see below)
VaryAccept-Language, X-Currency, Accept-Encoding, Cookie
Content-Security-PolicyGenerated per response with embedded nonce
RateLimit-Limit / RateLimit-Remaining / RateLimit-ResetRFC 9239
traceparentEchoed

4. Endpoint catalogue

MethodPath (after /bff/tenant-booking/v1/{tenantSlug})PurposeCacheIdempotent
GET/bootstrapTenant bootstrap (theme + flow + locales + currencies + payment + policies)edge: 30 s; app: 5 minyes
GET/policiesTenant policiesedge: 5 min; app: 1 hyes
GET/properties/{propertyId}Property summary (rooms, photos, policies)edge: 60 s; app: 5 minyes
GET/properties/{propertyId}/roomsRoom types with photosedge: 60 s; app: 5 minyes
GET/availabilityPer-room-type availability + cheapest rateedge: 30 s; app: 30 syes
POST/quoteIssue PriceQuotenoneyes (Idem-Key)
POST/holdCreate hold + bdr_ draftnoneyes (Idem-Key)
GET/draft/{draftId}Read current draft statenoneyes
PATCH/draft/{draftId}Update guest details / preferencesnoneyes (Idem-Key)
POST/draft/{draftId}/payment-intentCreate PaymentIntent + redirect URLnoneyes (Idem-Key)
POST/draft/{draftId}/returnPayment provider redirect-return; confirms reservationnoneyes (Idem-Key + replay-safe)
POST/draft/{draftId}/confirmConfirm via non-redirect rails (cash, MFS push)noneyes (Idem-Key)
DELETE/draft/{draftId}Abandon draft earlynoneyes
GET/confirmation/{reservationId}Composed confirmation viewedge: 30 s; app: 5 minyes
POST/handoff/consumeVerify + consume inbound handoff tokennoneyes (replay-safe)
POST/internal/handoff/{id}/consumeInternal endpoint exposed to bff-consumer-service (mutual-TLS + HMAC)noneyes
GET/sessionRead sanitized sessionnoneyes
POST/session/localeSet locale preferencenoneyes
POST/session/currencySet display currencynoneyes
POST/session/clearDrop session blob (logout-equivalent)noneyes
GET/health/liveLivenessnoneyes
GET/health/readyReadiness (Memorystore + Postgres + upstream circuits)noneyes

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

StatusCode
404MELMASTOON.BFF.TENANT.SLUG_UNKNOWN
503MELMASTOON.TENANT.SUSPENDED
410MELMASTOON.BFF.TENANT.BOOTSTRAP_STALE (when X-Bootstrap-Version is older than 7 days)
502MELMASTOON.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&currency=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

StatusCode
422MELMASTOON.GENERAL.VALIDATION_FAILED
422MELMASTOON.INVENTORY.MIN_LOS_VIOLATED / MAX_LOS_VIOLATED
502MELMASTOON.BFF.UPSTREAM_UNAVAILABLE
504MELMASTOON.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

StatusCode
404MELMASTOON.PRICING.RATE_PLAN_NOT_FOUND
409MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY
422MELMASTOON.PRICING.CURRENCY_MISMATCH
502MELMASTOON.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

StatusCode
410MELMASTOON.PRICING.QUOTE_EXPIRED
409MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY
409MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED
502MELMASTOON.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

StatusCode
502MELMASTOON.PAYMENT.GATEWAY_TIMEOUT
422MELMASTOON.PAYMENT.INTENT_NOT_FOUND (provider rejected)
422MELMASTOON.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

StatusCode
402MELMASTOON.PAYMENT.DECLINED
502MELMASTOON.BFF.TENANT.PAYMENT_RETURN_INVALID
504MELMASTOON.PAYMENT.GATEWAY_TIMEOUT
409MELMASTOON.RESERVATION.HOLD_EXPIRED
409MELMASTOON.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:

StatusCode
401MELMASTOON.BFF.TENANT.HANDOFF_SIGNATURE_INVALID
410MELMASTOON.BFF.TENANT.HANDOFF_EXPIRED
409MELMASTOON.BFF.TENANT.HANDOFF_REPLAYED
422MELMASTOON.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

BucketLimit
Per-tenant per-IP per-second50
Per-tenant per-IP per-minute600
Per-tenant global per-second200 (default; overridable per-tenant)
/quote per-session per-minute30
/hold per-session per-hour20
/payment-intent per-session per-hour6
/return per-session per-hour30 (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

EndpointCache-Control
/bootstrapprivate, max-age=30, stale-while-revalidate=60
/policiespublic, max-age=300, stale-while-revalidate=600
/availabilityprivate, max-age=15, stale-while-revalidate=30
/properties/...public, max-age=60, stale-while-revalidate=300
/quote /hold /draft/... /return /confirmno-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.