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:gen → openapi.json) and is the source of truth for SDK generation; this document is the canonical narrative.
1. Conventions
1.1 Common request headers
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <jwt> | yes (except /internal/*) | Issued by iam-service; contains tenant_id, roles[], optional property_ids[] |
X-Tenant-Id: tnt_… | yes | Cross-checked against JWT claim; mismatch → 403 MELMASTOON.TENANT.MISMATCH |
X-Property-Id: ppt_… | conditional | Required for property-scoped operator endpoints |
Idempotency-Key: <ULID> | yes on every mutating endpoint | 24 h dedupe window |
If-Match: "v<n>" | yes on DELETE /allocations/:id, PUT /policies/overbooking | OCC version |
traceparent: 00-… | optional | W3C trace context propagation |
Accept-Language | optional | Used 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
| Status | Code |
|---|---|
| 422 | MELMASTOON.API.PAYLOAD_TOO_LARGE, MELMASTOON.API.INVALID_INPUT |
| 403 | MELMASTOON.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 tocommitted.
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
| Status | Code |
|---|---|
| 409 | MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY, MELMASTOON.INVENTORY.STOP_SELL_ACTIVE, MELMASTOON.INVENTORY.OVERBOOKING_CAP_EXCEEDED, MELMASTOON.INVENTORY.ROOM_NOT_IN_TYPE |
| 422 | MELMASTOON.INVENTORY.HORIZON_EXHAUSTED, MELMASTOON.API.INVALID_INPUT |
| 503 | MELMASTOON.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
| Status | Code |
|---|---|
| 403 | MELMASTOON.IAM.FORBIDDEN |
| 404 | MELMASTOON.INVENTORY.ALLOCATION_NOT_FOUND |
| 409 | MELMASTOON.INVENTORY.STALE_VERSION, MELMASTOON.INVENTORY.ILLEGAL_TRANSITION |
| 422 | MELMASTOON.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
| Status | Code |
|---|---|
| 409 | MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY (with unmet[]) |
| 503 | MELMASTOON.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
| Status | Code |
|---|---|
| 403 | MELMASTOON.IAM.FORBIDDEN |
| 409 | MELMASTOON.INVENTORY.STALE_VERSION |
| 422 | MELMASTOON.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
| Method | Path | Caller | Purpose |
|---|---|---|---|
POST | /internal/events/reservation.held.v1 | Pub/Sub push | trigger PlaceHoldAllocationUseCase |
POST | /internal/events/reservation.confirmed.v1 | Pub/Sub push | trigger CommitAllocationUseCase |
POST | /internal/events/reservation.cancelled.v1 | Pub/Sub push | trigger ReleaseAllocationUseCase |
POST | /internal/events/reservation.dates_changed.v1 | Pub/Sub push | trigger atomic release+reallocate |
POST | /internal/events/reservation.no_show.v1 | Pub/Sub push | release per tenant.no_show.policy |
POST | /internal/events/reservation.hold_expired.v1 | Pub/Sub push | immediate release |
POST | /internal/events/property.room.created.v1 | Pub/Sub push | extend calendar lane |
POST | /internal/events/property.room.taken_out_of_order.v1 | Pub/Sub push | create block + reaccommodation |
POST | /internal/events/property.room.returned_to_service.v1 | Pub/Sub push | release block |
POST | /internal/events/tenant.settings.changed.v1 | Pub/Sub push | refresh policy cache |
POST | /internal/jobs/expire-holds | Cloud Scheduler | sweeper |
POST | /internal/jobs/extend-calendar-horizon | Cloud Scheduler | nightly extender |
POST | /internal/jobs/reconcile-calendar-summary | Cloud Scheduler | nightly reconcile |
GET | /internal/health | platform | liveness |
GET | /internal/ready | platform | readiness (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
- Use cases backing each endpoint: APPLICATION_LOGIC §2
- Event subjects emitted by these use cases: EVENT_SCHEMAS
- Auth/authz matrix: SECURITY_MODEL §2
- Data shapes: DATA_MODEL
- Platform API conventions: 05 API design
- Canonical error codes: standards/ERROR_CODES