Skip to main content

API_CONTRACTS — inventory-service

Sibling: APPLICATION_LOGIC · EVENT_SCHEMAS · SECURITY_MODEL · DATA_MODEL

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

All endpoints are URI-versioned under /api/v1/inventory. Internal Pub/Sub push handlers live under /internal/events/*. Health/readiness under /internal/health and /internal/ready. The OpenAPI definition is generated from controller decorators (pnpm openapi:genopenapi.json) and is the source of truth for SDK generation; this document is the canonical narrative.


1. Conventions

1.1 Common request headers

HeaderRequiredNotes
Authorization: Bearer <jwt>yes (except /internal/*)Issued by iam-service; contains tenant_id, roles[], optional property_ids[]
X-Tenant-Id: tnt_…yesCross-checked against JWT claim; mismatch → 403 MELMASTOON.TENANT.MISMATCH
X-Property-Id: ppt_…conditionalRequired for property-scoped operator endpoints
Idempotency-Key: <ULID>yes on every mutating endpoint24 h dedupe window
If-Match: "v<n>"yes on DELETE /allocations/:id, PUT /policies/overbookingOCC version
traceparent: 00-…optionalW3C trace context propagation
Accept-LanguageoptionalUsed only in error.userMessageKey translation

1.2 Error envelope (RFC 7807 Problem+JSON, platform-canonical)

{
"error": {
"type": "https://errors.melmastoon.ghasi.io/inventory/<code>",
"code": "MELMASTOON.INVENTORY.<CODE>",
"title": "Insufficient availability",
"status": 409,
"detail": "Only 2 rooms of type rty_… are available for night 2026-05-04",
"instance": "/api/v1/inventory/allocations",
"traceId": "00-…",
"requestId": "01J…",
"tenantId": "tnt_…",
"retriable": false,
"userMessageKey": "errors.inventory.insufficient_availability",
"docUrl": "https://docs.melmastoon.ghasi.io/errors/inventory/insufficient_availability",
"runbook": "https://runbooks.melmastoon.ghasi.io/inventory/insufficient_availability"
}
}

Canonical codes are listed in APPLICATION_LOGIC §8 and standards/ERROR_CODES.

1.3 Pagination

Cursor-based: requests accept ?cursor=<opaque>&limit=<1..200>; responses return { items: [...], nextCursor?: '...' }. Cursors are signed and tenant-scoped.

1.4 OCC

Every mutating endpoint that targets an existing aggregate (DELETE /allocations/:id, DELETE /blocks/:id, PUT /policies/overbooking) requires If-Match: "v<version>". Mismatch → 409 MELMASTOON.INVENTORY.STALE_VERSION. The current version is exposed in ETag: "v<n>" on every read response.


2. Endpoint reference

2.1 POST /api/v1/inventory/availability/search

Multi-property availability lookup. Used by bff-tenant-booking-service (funnel), bff-backoffice-service (ad-hoc check), search-aggregation-service (cache rebuild), and the meta consumer via bff-consumer-service.

Request

{
"propertyIds": ["ppt_…", "ppt_…"],
"stayWindow": {
"checkIn": "2026-05-04",
"checkOut": "2026-05-07"
},
"occupancy": { "adults": 2, "children": 1, "infants": 0 },
"roomTypeIds": ["rty_…"], // optional filter
"rooms": 1, // qty per room-type entry; default 1
"includeBlocks": false // staff/operator only; ignored otherwise
}

Response 200

{
"results": [
{
"propertyId": "ppt_…",
"stayWindow": { "checkIn": "2026-05-04", "checkOut": "2026-05-07" },
"roomTypes": [
{
"roomTypeId": "rty_…",
"available": 3,
"stopSell": false,
"perNight": [
{ "stayDate": "2026-05-04", "available": 3 },
{ "stayDate": "2026-05-05", "available": 3 },
{ "stayDate": "2026-05-06", "available": 2 }
]
}
]
}
],
"queriedAt": "2026-05-01T08:21:14Z",
"cacheHit": true
}

Constraints: ≤ 50 properties per call, ≤ 90 nights span. Beyond these → 422 MELMASTOON.API.PAYLOAD_TOO_LARGE.

Errors

StatusCode
422MELMASTOON.API.PAYLOAD_TOO_LARGE, MELMASTOON.API.INVALID_INPUT
403MELMASTOON.TENANT.MISMATCH

2.2 POST /api/v1/inventory/allocations

Create an allocation. Two execution modes:

  • Saga-driven (event): not exposed via REST; handled by /internal/events/reservation.held.v1.
  • Walk-in (sync REST): operator-driven, body sets mode='walk_in'; allocation goes straight to committed.

Request (walk-in)

{
"mode": "walk_in",
"tenantId": "tnt_…",
"propertyId": "ppt_…",
"roomTypeId": "rty_…",
"roomId": "rom_…", // optional; if omitted → auto-pick
"stayWindow": { "checkIn": "2026-05-04", "checkOut": "2026-05-05" },
"occupancy": { "adults": 2, "children": 0, "infants": 0 },
"reservationId": "rsv_…",
"reservationItemId": "rsi_…",
"assignmentSource": "staff",
"notes": "Guest requested ground floor"
}

Response 201

{
"allocationId": "inv_…",
"status": "committed",
"tenantId": "tnt_…",
"propertyId": "ppt_…",
"roomTypeId": "rty_…",
"roomId": "rom_…",
"reservationId": "rsv_…",
"reservationItemId": "rsi_…",
"stayWindow": { "checkIn": "2026-05-04", "checkOut": "2026-05-05" },
"committedAt": "2026-05-04T14:02:11Z",
"version": 1
}

Headers: ETag: "v1", Location: /api/v1/inventory/allocations/inv_….

Errors

StatusCode
409MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY, MELMASTOON.INVENTORY.STOP_SELL_ACTIVE, MELMASTOON.INVENTORY.OVERBOOKING_CAP_EXCEEDED, MELMASTOON.INVENTORY.ROOM_NOT_IN_TYPE
422MELMASTOON.INVENTORY.HORIZON_EXHAUSTED, MELMASTOON.API.INVALID_INPUT
503MELMASTOON.INVENTORY.LOCK_TIMEOUT (Retry-After: 1)

2.3 GET /api/v1/inventory/allocations/:allocationId

Response 200

{
"allocationId": "inv_…",
"status": "committed",
"mode": "auto_pick",
"tenantId": "tnt_…",
"propertyId": "ppt_…",
"roomTypeId": "rty_…",
"roomId": "rom_…",
"reservationId": "rsv_…",
"reservationItemId": "rsi_…",
"stayWindow": { "checkIn": "2026-05-04", "checkOut": "2026-05-07" },
"heldUntil": null,
"committedAt": "2026-05-01T08:25:11Z",
"releasedAt": null,
"releaseReasonCode": null,
"version": 2
}

ETag: "v2". Staff-only endpoint.


2.4 DELETE /api/v1/inventory/allocations/:allocationId

Manual release. Restricted to gm/owner. Requires If-Match: "v<n>". Body must include a justification.

Request

{
"reasonCode": "staff_manual_release",
"justification": "Guest no-call no-show; manual release after 6h grace"
}

Response 200

{
"allocationId": "inv_…",
"status": "released",
"releasedAt": "2026-05-04T20:11:00Z",
"releaseReasonCode": "staff_manual_release",
"version": 3
}

Errors

StatusCode
403MELMASTOON.IAM.FORBIDDEN
404MELMASTOON.INVENTORY.ALLOCATION_NOT_FOUND
409MELMASTOON.INVENTORY.STALE_VERSION, MELMASTOON.INVENTORY.ILLEGAL_TRANSITION
422MELMASTOON.API.INVALID_INPUT (missing justification)

Idempotency: re-requesting on an already-released allocation returns 200 with the same body and releasedAt unchanged.


2.5 POST /api/v1/inventory/blocks

Create an OOO/OOS/maintenance block.

Request

{
"tenantId": "tnt_…",
"propertyId": "ppt_…",
"roomId": "rom_…",
"stayWindow": { "checkIn": "2026-05-10", "checkOut": "2026-05-13" },
"reason": "ooo",
"reasonText": "AC compressor replacement",
"source": { "kind": "staff", "actorId": "usr_…" }
}

Response 202 (asynchronous reaccommodation if needed)

{
"blockId": "blk_…",
"status": "active",
"createdAt": "2026-05-04T11:30:00Z",
"affectedReservations": [
{ "reservationId": "rsv_…", "reservationItemId": "rsi_…", "overlapNights": ["2026-05-11"] }
],
"reaccommodationRequired": true,
"version": 1
}

If no overlapping committed allocations: 201 Created, affectedReservations: [], reaccommodationRequired: false.


2.6 DELETE /api/v1/inventory/blocks/:blockId

Releases a block. Requires If-Match.

Response 200

{
"blockId": "blk_…",
"status": "released",
"releasedAt": "2026-05-12T09:00:00Z",
"version": 2
}

2.7 GET /api/v1/inventory/blocks

List blocks. Filters: propertyId (required), range.from, range.to, roomId?, reason?, status?. Pagination per §1.3.

Response 200

{
"items": [
{
"blockId": "blk_…",
"tenantId": "tnt_…",
"propertyId": "ppt_…",
"roomId": "rom_…",
"stayWindow": { "checkIn": "2026-05-10", "checkOut": "2026-05-13" },
"reason": "ooo",
"reasonText": "AC compressor replacement",
"status": "active",
"createdAt": "2026-05-04T11:30:00Z",
"version": 1
}
],
"nextCursor": null
}

2.8 GET /api/v1/inventory/properties/:propertyId/calendar

Day-grid view for backoffice. Default range: today + 30 days.

Query: ?from=YYYY-MM-DD&to=YYYY-MM-DD&roomTypeIds=rty_…,rty_…

Response 200

{
"propertyId": "ppt_…",
"days": [
{
"stayDate": "2026-05-04",
"summary": { "totalRooms": 12, "availableRooms": 7, "heldRooms": 1, "committedRooms": 4, "blockedRooms": 0 },
"stopSell": false,
"byRoomType": [
{ "roomTypeId": "rty_…", "available": 3, "held": 1, "committed": 2, "oosBlocked": 0, "stopSell": false }
],
"blocks": []
}
]
}

2.9 POST /api/v1/inventory/groups/holds

Atomic group hold; all-or-nothing.

Request

{
"tenantId": "tnt_…",
"propertyId": "ppt_…",
"groupId": "grp_…",
"items": [
{ "roomTypeId": "rty_…", "qty": 2, "stayWindow": { "checkIn": "2026-05-04", "checkOut": "2026-05-07" } },
{ "roomTypeId": "rty_…", "qty": 1, "stayWindow": { "checkIn": "2026-05-04", "checkOut": "2026-05-07" } }
],
"ttlSeconds": 1200,
"reservationId": "rsv_…"
}

Response 201

{
"groupHoldId": "ghd_…",
"allocations": [
{ "allocationId": "inv_…", "roomTypeId": "rty_…", "roomId": null, "status": "held", "heldUntil": "2026-05-04T08:50:00Z" },
{ "allocationId": "inv_…", "roomTypeId": "rty_…", "roomId": null, "status": "held", "heldUntil": "2026-05-04T08:50:00Z" },
{ "allocationId": "inv_…", "roomTypeId": "rty_…", "roomId": null, "status": "held", "heldUntil": "2026-05-04T08:50:00Z" }
]
}

Errors

StatusCode
409MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY (with unmet[])
503MELMASTOON.INVENTORY.LOCK_TIMEOUT

2.10 GET /api/v1/inventory/properties/:propertyId/policies/overbooking

Read overbooking policy.

Response 200

{
"policyId": "obp_…",
"tenantId": "tnt_…",
"enabled": false,
"cap": 0,
"roomTypeIds": [],
"alertRoutes": [],
"effectiveFrom": "2026-01-01T00:00:00Z",
"effectiveUntil": null,
"version": 1
}

ETag: "v1".


2.11 PUT /api/v1/inventory/properties/:propertyId/policies/overbooking

Update policy. Restricted to gm/owner. Requires If-Match.

Request

{
"enabled": true,
"cap": 2,
"roomTypeIds": ["rty_…"],
"alertRoutes": ["pagerduty:gm-on-call", "email:owner"],
"effectiveFrom": "2026-05-01T00:00:00Z",
"effectiveUntil": "2026-05-31T23:59:59Z"
}

Response 200: same shape as 2.10 with version bumped.

Errors

StatusCode
403MELMASTOON.IAM.FORBIDDEN
409MELMASTOON.INVENTORY.STALE_VERSION
422MELMASTOON.INVENTORY.OVERBOOKING_CAP_EXCEEDED (negative cap or no alert routes when enabled)

2.12 GET /api/v1/inventory/properties/:propertyId/snapshot

Used only by sync-service for the desktop pull. Returns the full per-room-type per-day snapshot for the configured 30-day-forward + 7-day-back window. See SYNC_CONTRACT for shape.


3. Internal endpoints

MethodPathCallerPurpose
POST/internal/events/reservation.held.v1Pub/Sub pushtrigger PlaceHoldAllocationUseCase
POST/internal/events/reservation.confirmed.v1Pub/Sub pushtrigger CommitAllocationUseCase
POST/internal/events/reservation.cancelled.v1Pub/Sub pushtrigger ReleaseAllocationUseCase
POST/internal/events/reservation.dates_changed.v1Pub/Sub pushtrigger atomic release+reallocate
POST/internal/events/reservation.no_show.v1Pub/Sub pushrelease per tenant.no_show.policy
POST/internal/events/reservation.hold_expired.v1Pub/Sub pushimmediate release
POST/internal/events/property.room.created.v1Pub/Sub pushextend calendar lane
POST/internal/events/property.room.taken_out_of_order.v1Pub/Sub pushcreate block + reaccommodation
POST/internal/events/property.room.returned_to_service.v1Pub/Sub pushrelease block
POST/internal/events/tenant.settings.changed.v1Pub/Sub pushrefresh policy cache
POST/internal/jobs/expire-holdsCloud Schedulersweeper
POST/internal/jobs/extend-calendar-horizonCloud Schedulernightly extender
POST/internal/jobs/reconcile-calendar-summaryCloud Schedulernightly reconcile
GET/internal/healthplatformliveness
GET/internal/readyplatformreadiness (DB + Pub/Sub)

All /internal/* reject calls without the expected GCP service-account principal in the verified IAM token.


4. Versioning

URI-versioning (/api/v1). Breaking changes require /api/v2 and a 6-month dual-version overlap. Non-breaking additions (new optional response fields) ship in-place without a version bump. The OpenAPI snapshot diff in CI gates breaking changes.


5. Cross-references