maintenance-service · API_CONTRACTS
Base path:
/api/v1/maintenance· OpenAPI source of truth:services/maintenance-service/openapi/v1.yaml(generated from NestJS decorators on each release) · Error envelope: RFC 7807 Problem+JSON withMELMASTOON.MAINTENANCE.*codes.
1. Common headers
| Header | Required | Purpose |
|---|---|---|
Authorization: Bearer <jwt> | ✅ | Caller identity (BFF or service-to-service) |
X-Tenant-Id: tnt_<ULID> | ✅ | Tenant scope; cross-checked against JWT claim |
X-Property-Id: prop_<ULID> | for property-scoped commands | Property scope |
X-Correlation-Id: <ULID> | optional | Saga / trace correlation |
X-Idempotency-Key: <client-uuid> | for unsafe POSTs from BFF | Idempotent retry support |
If-Match: <version> | on PATCH and state transitions | OCC token |
2. Error envelope (RFC 7807)
{
"type": "https://errors.melmastoon.app/MELMASTOON.MAINTENANCE.INVALID_STATUS_TRANSITION",
"title": "Invalid status transition",
"status": 409,
"code": "MELMASTOON.MAINTENANCE.INVALID_STATUS_TRANSITION",
"detail": "Cannot transition from 'open' to 'resolved'",
"instance": "/api/v1/maintenance/work-orders/mnt_01HXY...3K/resolve",
"correlationId": "01HXYZABCDEF...",
"i18nKey": "errors.maintenance.invalid_status_transition",
"context": { "from": "open", "to": "resolved" }
}
Canonical codes used by this service (full list and HTTP mapping in docs/standards/ERROR_CODES.md):
| Code | HTTP |
|---|---|
MELMASTOON.MAINTENANCE.INVALID_STATUS_TRANSITION | 409 |
MELMASTOON.MAINTENANCE.SEVERITY_REQUIRES_TARGET | 422 |
MELMASTOON.MAINTENANCE.DUPLICATE_OPEN_WORK_ORDER | 409 |
MELMASTOON.MAINTENANCE.COST_CURRENCY_MISMATCH | 422 |
MELMASTOON.MAINTENANCE.VENDOR_CHANNEL_MISMATCH | 422 |
MELMASTOON.MAINTENANCE.PART_OUT_OF_STOCK | 409 |
MELMASTOON.MAINTENANCE.SCHEDULE_NEXT_DUE_REGRESSION | 422 |
MELMASTOON.MAINTENANCE.PREVENTIVE_DUPLICATE_FIRE | 409 (returned silently as 200 internally) |
MELMASTOON.MAINTENANCE.WORK_ORDER_TERMINAL | 409 |
MELMASTOON.MAINTENANCE.ASSET_NOT_FOUND | 404 |
MELMASTOON.MAINTENANCE.VENDOR_NOT_FOUND | 404 |
MELMASTOON.MAINTENANCE.WORK_ORDER_NOT_FOUND | 404 |
MELMASTOON.SYS.OCC_CONFLICT | 409 |
MELMASTOON.IAM.AUTHZ_DENIED | 403 |
MELMASTOON.IAM.AUTHN_REQUIRED | 401 |
3. Resources
3.1 POST /api/v1/maintenance/work-orders
Request:
{
"propertyId": "prop_01HXP...",
"roomId": "room_01HXR...",
"assetId": null,
"category": "hvac",
"severity": "high",
"title": "AC blowing warm air in 204",
"description": "Guest reports AC has been warm since check-in around 14:00. Filter looks dusty.",
"source": "guest_complaint",
"originRef": null,
"allowDuplicate": false,
"aiAssist": { "categoryClassify": false, "severitySuggest": false }
}
Response 201:
{
"id": "mnt_01HXY1...",
"status": "open",
"severity": "high",
"category": "hvac",
"version": 1,
"causedRoomBlock": true,
"relocationRequired": false,
"slaTimer": { "targetMinutes": 240, "dueAt": "2026-04-22T18:00:00Z" },
"aiProvenance": []
}
Errors: 400 INVALID_INPUT, 404 ASSET_NOT_FOUND, 409 DUPLICATE_OPEN_WORK_ORDER, 422 SEVERITY_REQUIRES_TARGET.
3.2 GET /api/v1/maintenance/work-orders/:id
Returns the full WO including tasks[], partsUsed[], costLines[], vendorAcknowledgement, vendorInvoice, aiProvenance[], version.
3.3 GET /api/v1/maintenance/work-orders
Query parameters:
| Param | Type | Notes |
|---|---|---|
propertyId | ULID | required for non-tenant-admin callers |
status | open|assigned|in_progress|blocked|resolved|verified|cancelled (repeatable) | |
severity | repeatable | |
category | repeatable | |
assetId | ULID | |
roomId | ULID | |
assigneeUserId | ULID | |
vendorId | ULID | |
source | repeatable | |
dateFrom, dateTo | ISO-8601 | filters on createdAt |
slaState | on_track|breach_imminent|breached | derived |
cursor | opaque | pagination |
limit | int 1..100, default 50 |
Response: { items: WorkOrder[], cursor: string|null }.
3.4 PATCH /api/v1/maintenance/work-orders/:id
Mutates only narrow editable fields (title, description, severity, category, customCategoryId, assetId, roomId). Requires If-Match.
3.5 POST /api/v1/maintenance/work-orders/:id/assign
{
"assignee": { "kind": "vendor", "vendorId": "vnd_01HXV..." },
"acknowledgeManualNotify": true,
"note": "Habibi Electrical, asked for callback 5pm"
}
Response 200: { id, status: "assigned", version }.
3.6 POST /api/v1/maintenance/work-orders/:id/start
Empty body. Response 200: { id, status: "in_progress", version }.
3.7 POST /api/v1/maintenance/work-orders/:id/block
{ "reason": "awaiting_part", "etaIso": "2026-04-23T10:00:00+04:30", "note": "Compressor capacitor on order from Peshawar" }
3.8 POST /api/v1/maintenance/work-orders/:id/resume
{ "note": "Part arrived, resuming" }
3.9 POST /api/v1/maintenance/work-orders/:id/resolve
{
"resolutionNote": "Replaced capacitor; airflow restored.",
"costLines": [
{ "kind": "labor", "description": "2 staff × 90 min", "amount": { "currency": "AFN", "amountMicro": "150000000" }, "minutes": 180 },
{ "kind": "vendor_invoice", "description": "Habibi Electrical service call", "amount": { "currency": "AFN", "amountMicro": "300000000" } }
],
"partsUsed": [
{ "partId": "prt_01HXP...", "quantity": 1 }
]
}
Response 200: { id, status: "resolved", version, costRollup: { currency, amountMicro } }.
Errors: 409 PART_OUT_OF_STOCK, 422 COST_CURRENCY_MISMATCH.
3.10 POST /api/v1/maintenance/work-orders/:id/verify
{ "note": "Inspected room, AC cold. Released to housekeeping." }
RBAC: gm or owner. Releases the room block (if any) and triggers preventive next-due update.
3.11 POST /api/v1/maintenance/work-orders/:id/cancel
{ "reason": "Issue resolved itself; guest no longer reporting." }
3.12 POST /api/v1/maintenance/work-orders/:id/escalate
{ "reason": "Vendor missed second appointment", "target": { "kind": "user", "userId": "usr_01HXG..." } }
target.kind ∈ user | role; role looks up tenant escalation chain.
3.13 POST /api/v1/maintenance/work-orders/:id/parts-usage
Append a part usage standalone (without resolve).
{ "partId": "prt_01HXP...", "quantity": 2, "note": "scrapped first attempt" }
3.14 POST /api/v1/maintenance/work-orders/:id/vendor-acknowledged
{ "channel": "phone", "note": "Vendor confirmed visit at 5pm tomorrow" }
3.15 POST /api/v1/maintenance/work-orders/:id/vendor-invoice
{
"amount": { "currency": "AFN", "amountMicro": "500000000" },
"invoiceNumber": "HE-2026-0418",
"issuedAt": "2026-04-22",
"dueAt": "2026-05-22",
"fileRef": "gs://melmastoon-vendor-invoices/tnt_x/2026/HE-2026-0418.pdf"
}
3.16 Preventive schedules
GET /api/v1/maintenance/preventive-schedules
POST /api/v1/maintenance/preventive-schedules
GET /api/v1/maintenance/preventive-schedules/:id
PATCH /api/v1/maintenance/preventive-schedules/:id
DELETE /api/v1/maintenance/preventive-schedules/:id
POST /api/v1/maintenance/preventive-schedules/:id/trigger-now
POST body example:
{
"propertyId": "prop_01HXP...",
"assetId": "ast_01HXG...",
"category": "generator",
"title": "Generator A — 250-hour service",
"cadence": { "kind": "composite", "everyHours": 250, "everyDays": 90, "tz": "Asia/Kabul" },
"slaTargetMinutes": 1440,
"defaultSeverity": "normal",
"defaultAssignee": { "kind": "vendor", "vendorId": "vnd_01HXV..." },
"notifyChannel": "whatsapp",
"active": true
}
3.17 Assets
GET /api/v1/maintenance/assets
POST /api/v1/maintenance/assets
GET /api/v1/maintenance/assets/:id
PATCH /api/v1/maintenance/assets/:id
DELETE /api/v1/maintenance/assets/:id
POST /api/v1/maintenance/assets/:id/health-update
POST /assets:
{
"propertyId": "prop_01HXP...",
"roomId": null,
"class": "generator",
"displayName": "Main Generator (Cummins 25 kVA)",
"model": "C25D5",
"serialNumber": "CUM-25D5-AFG-117",
"installedAt": "2024-08-12",
"capacity": { "value": 25, "unit": "kVA" }
}
POST /assets/:id/health-update:
{
"runHoursDelta": 47,
"healthIndexOverride": null,
"note": "Generator ran 47h since last reading; oil pressure normal."
}
3.18 Vendors
GET /api/v1/maintenance/vendors
POST /api/v1/maintenance/vendors
GET /api/v1/maintenance/vendors/:id
PATCH /api/v1/maintenance/vendors/:id
DELETE /api/v1/maintenance/vendors/:id
POST /vendors:
{
"displayName": "Habibi Electrical",
"categories": ["electrical", "generator", "hvac"],
"contactName": "Habibi Khan",
"phoneE164": "+93700123456",
"whatsappE164":"+93700123456",
"email": null,
"channelPreference": { "primary": "whatsapp", "fallback": "sms" },
"addressFreeText": "Shahre Naw, Kabul",
"callbackWindows": [{ "startsAt": "09:00", "endsAt": "17:00", "note": "weekdays only" }]
}
3.19 Parts (light inventory)
GET /api/v1/maintenance/parts
POST /api/v1/maintenance/parts
GET /api/v1/maintenance/parts/:id
PATCH /api/v1/maintenance/parts/:id
{
"propertyId": "prop_01HXP...",
"partNumber": "FILTER-HVAC-12X12",
"displayName": "HVAC dust filter 12×12",
"category": "hvac",
"onHand": 24,
"reorderThreshold": 6,
"lastUnitCost": { "currency": "AFN", "amountMicro": "5000000" }
}
4. Internal endpoints (Pub/Sub push & cron, not in public OpenAPI)
| Path | Source | Effect |
|---|---|---|
POST /internal/pubsub/maintenance/housekeeping-required | melmastoon.housekeeping.room.maintenance_required.v1 | Auto-create WO |
POST /internal/pubsub/maintenance/lock-health-alert | melmastoon.lock_integration.device.health_alert.v1 | Upsert asset + auto-create WO |
POST /internal/pubsub/maintenance/property-room-ooo | melmastoon.property.room.taken_out_of_order.v1 | Link OOO to WOs |
POST /internal/pubsub/maintenance/property-room-released | melmastoon.property.room.returned_to_service.v1 | Sanity-check release |
POST /internal/pubsub/maintenance/staff-shift-started | melmastoon.staff.shift.started.v1 | Refresh roster cache |
POST /internal/pubsub/maintenance/tenant-settings-changed | melmastoon.tenant.settings.changed.v1 | Refresh SLA / escalation cache |
POST /internal/pubsub/maintenance/reservation-checked-in | melmastoon.reservation.checked_in.v1 | Re-evaluate active WOs on the room |
POST /internal/pubsub/maintenance/billing-vendor-invoice-posted | melmastoon.billing.vendor_invoice.posted.v1 | Mark vendorInvoice.postedToFolio |
POST /internal/cron/preventive-scheduler | Cloud Scheduler 1× per minute | Materialise due preventive WOs |
POST /internal/cron/sla-breach-scanner | Cloud Scheduler 1× per minute | Detect & emit SLA breaches |
POST /internal/cron/vendor-reminder | Cloud Scheduler 1× per 5 min | Re-notify pending vendors |
POST /internal/cron/asset-health-forecaster | Cloud Scheduler 1× per hour | AI-update asset health indices |
All internal/* endpoints require IAM-auth via Workload Identity, never user-issued JWT.
5. Pagination
- Cursor-based;
cursoris opaque base64-encoded(createdAt, id)tuple. limit1..100; default 50.- Reverse iteration not supported in v1 (use
dateTo/dateFrominstead).
6. Idempotency
- All state-changing endpoints accept
X-Idempotency-Key. Same key + same body within 24 h returns the original response. - Implementation:
idempotency_keystable keyed(tenant_id, key, route).
7. Versioning
- v1 is the only published version.
- Breaking changes require a v2 path; old v1 path remains for at least 6 months with deprecation header.
- Additive changes to response bodies are non-breaking and rolled out immediately.
8. Rate limits
| Caller type | Default | Burst |
|---|---|---|
BFF (per tenantId) | 50 RPS | 100 |
| Pub/Sub push | unlimited (Cloud Run handles backpressure) | — |
| Cron internal | unlimited | — |
Limits enforced at Kong; codes returned: 429 MELMASTOON.SYS.RATE_LIMITED with Retry-After header.