Skip to main content

API_CONTRACTS — bff-backoffice-service

Sibling: APPLICATION_LOGIC · EVENT_SCHEMAS · SECURITY_MODEL · SYNC_CONTRACT

Cross-cutting: Standards · NAMING · Standards · ERROR_CODES · 05 API Design · ADR-0003

The primary deliverable for this BFF. The API surface is consumed exclusively by the Electron desktop backoffice application through its narrow contextBridge (window.melmastoon.*). All endpoints are versioned under /bff/backoffice/v1/. All request/response shapes are codified in OpenAPI; the file in openapi/openapi.yaml is the single source of truth and is regenerated on every release.

1. Base URLs

EnvironmentBase URL
Productionhttps://backoffice.melmastoon.ghasi.io/api/bff/backoffice/v1
Stagehttps://stage-backoffice.melmastoon.ghasi.io/api/bff/backoffice/v1
Dev (per-developer)https://localhost:8083/api/bff/backoffice/v1
Internal canonicalhttps://api.melmastoon.ghasi.io/bff/backoffice/v1 (also served, for reverse-proxy traffic)

2. Authentication

StageMechanism
Sign-inDesktop POST → iam-service.signIn directly (BFF NOT on critical sign-in path) — issues device-bound JWT (access 15 min) + refresh token (30 d) bound to device
RefreshPOST /auth/refresh on this BFF — proxies to iam-service.refresh with proof-of-possession
SubsequentAuthorization: Bearer <access-token> on every request; access token carries cnf.jkt (JWK thumbprint) of device key
Sign-outPOST /auth/sign-out

Device-bound JWT (RFC 8705 / DPoP-style):

  • Each request carries Authorization: Bearer <jwt> and DPoP: <signed-pop> headers.
  • BFF verifies cnf.jkt matches the DPoP key thumbprint.
  • Tokens stolen without device key are unusable.

3. Common request headers

HeaderRequiredDescription
Authorization: Bearer <jwt>yes (except /health/*)Device-bound access token
DPoP: <signed-pop>yesSingle-use proof-of-possession token
X-Request-Idrecommended (BFF mints if absent)ULID; idempotency aid
X-Idempotency-Keyrequired on mutationsULID; 24 h scope
X-Device-IdyesEchoes JWT cnf.kid for cross-check
X-App-Versionyese.g. 1.4.2
X-App-Platformyeswin32 / darwin / linux
X-Network-Profileoptionalgood / flaky / offline-recently (informs telemetry)
X-Property-Idrequired for property-scoped readsSelected property
Accept-LanguageoptionalBCP-47; defaults to operator preferences locale
traceparentoptionalW3C trace propagation
Content-Type: application/jsonyes (mutations)

4. Common response headers

HeaderDescription
X-Request-IdEchoed
X-Composed-AtISODateTime when composed (cache age aid)
`X-Cache: HITMISS
X-Bff-VersionBuild SHA
Cache-Control: no-storeDefault for authenticated endpoints
traceparentW3C trace
Content-LanguageResolved locale

5. Error envelope

Per ERROR_CODES:

{
"error": {
"code": "MELMASTOON.<DOMAIN>.<CODE>",
"message": "Human-readable for operators (English baseline + localized variant when locale is non-en)",
"userMessage": "Localized message safe to render in UI",
"requestId": "req_01H8YN7QF9RTBRZG4F8Y5CK4MV",
"traceId": "00-...-...-01",
"details": { "fieldErrors": [] },
"retryable": false,
"retryAfterSeconds": null,
"documentationUrl": "https://errors.melmastoon.ghasi.io/MELMASTOON.BFF.BACKOFFICE.SUGGESTION_NOT_FOUND"
}
}

5.1 Common error codes

CodeHTTPWhen
MELMASTOON.IAM.SESSION_REQUIRED401No / invalid Authorization
MELMASTOON.IAM.SESSION_EXPIRED401Access token expired (refresh path)
MELMASTOON.IAM.DPOP_INVALID401DPoP proof signature / nonce mismatch
MELMASTOON.BFF.BACKOFFICE.DEVICE_MISMATCH403X-Device-Id ≠ JWT cnf.kid
MELMASTOON.GENERAL.CROSS_TENANT_REFERENCE403Resource tenant ≠ JWT tenant
MELMASTOON.IAM.NO_PROPERTY_SCOPE403Operator has no property scope
MELMASTOON.BFF.BACKOFFICE.PROPERTY_OUT_OF_SCOPE403Operator not granted on requested property
MELMASTOON.BFF.BACKOFFICE.MFA_REQUIRED401Step-up required
MELMASTOON.BFF.BACKOFFICE.MFA_INVALID_OR_USED401Attestation invalid or replayed
MELMASTOON.BFF.BACKOFFICE.SUGGESTION_NOT_FOUND404Suggestion id unknown
MELMASTOON.BFF.BACKOFFICE.DECISION_ALREADY_RECORDED409Suggestion already decided
MELMASTOON.BFF.BACKOFFICE.ALERT_ALREADY_ACKED409Alert already acked
MELMASTOON.BFF.BACKOFFICE.PREFERENCES_CONFLICT412Optimistic concurrency
MELMASTOON.GENERAL.RATE_LIMITED429Per-device rate limit
MELMASTOON.BFF.UPSTREAM_UNAVAILABLE503Circuit open
MELMASTOON.BFF.UPSTREAM_TIMEOUT504Per-call deadline exceeded
MELMASTOON.BFF.SCHEMA_DRIFT502Upstream payload failed schema parse
MELMASTOON.BFF.CACHE_UNAVAILABLE503Memorystore session tier down
MELMASTOON.GENERAL.PRECONDITION_FAILED412Idem-key reused with different body

6. Versioning

  • URL major: /v1/. Breaking changes ship at /v2/ with overlap.
  • Minor changes additive; verified by Pact + OpenAPI diff CI gate.
  • Deprecated fields emit Deprecation + Sunset headers.

7. Endpoint catalogue (overview)

GroupMethodPath
AuthPOST/auth/refresh
AuthPOST/auth/sign-out
DashboardGET/dashboard
WorkbenchGET/today
WorkbenchGET/arrivals
WorkbenchGET/departures
WorkbenchGET/in-house
WorkbenchGET/housekeeping/board
WorkbenchGET/maintenance/board
AIGET/ai/suggestions
AIPOST/ai/suggestions/{id}/decide
AlertsGET/alerts
AlertsPOST/alerts/{id}/acknowledge
PreferencesGET/preferences
PreferencesPUT/preferences
DevicesPOST/devices/{deviceId}/heartbeat
SyncGET/sync/cursor
SyncPOST/sync/cursor
SyncPOST/sync/handshake
LocksPOST/locks/{reservationId}/issue-key
LocksPOST/locks/{reservationId}/revoke-key
MutationsPOST/reservations/{id}/check-in
MutationsPOST/reservations/{id}/check-out
MutationsPOST/reservations/{id}/cancel
MutationsPOST/folios/{id}/charges
MutationsPOST/housekeeping/tasks/{id}/transition
MutationsPOST/maintenance/work-orders/{id}/transition
MFAPOST/auth/mfa/step-up
SSEGET/sse/stream
HealthGET/health/live
HealthGET/health/ready

8. POST /auth/refresh

Request

POST /api/bff/backoffice/v1/auth/refresh HTTP/1.1
Host: backoffice.melmastoon.ghasi.io
Content-Type: application/json
DPoP: <jwt-signed-with-device-key>
X-Device-Id: dev_01H8Y...
X-App-Version: 1.4.2
X-App-Platform: win32

{
"refreshToken": "rft_…",
"devicePoP": "<DPoP proof>"
}

200 Response

{
"accessToken": "eyJ...",
"accessTokenExpiresAt": "2026-04-23T09:29:22Z",
"refreshToken": "rft_...",
"refreshTokenExpiresAt": "2026-05-23T09:14:22Z",
"operator": {
"id": "opr_01H8Y...",
"displayName": "Sayed Husain",
"roles": ["front_desk_supervisor"],
"propertyScope": ["prop_01H8Y..."]
},
"tenantId": "tnt_01H8Y..."
}

Errors

401 SESSION_EXPIRED (refresh expired), 401 DPOP_INVALID, 403 IAM.DEVICE_REVOKED, 503 UPSTREAM_UNAVAILABLE.

9. GET /dashboard?propertyId=<id>

Composed snapshot. Per-widget skeletons when partial.

200 Response (full)

{
"id": "dsh_01H8Y...",
"tenantId": "tnt_01H8Y...",
"propertyId": "prop_01H8Y...",
"operatorRole": "front_desk_supervisor",
"composedAt": "2026-04-23T09:14:22Z",
"partial": false,
"widgets": {
"today": { "status": "fresh", "data": { "checkInsExpected": 12, "checkOutsExpected": 9, "noShowsLast24h": 0 } },
"arrivals": { "status": "fresh", "data": { "items": [/* 12 */], "total": 12 } },
"departures": { "status": "fresh", "data": { "items": [/* 9 */], "total": 9 } },
"inHouse": { "status": "fresh", "data": { "items": [/* 41 */], "total": 41 } },
"occupancy": { "status": "fresh", "data": { "occupancyPct": 78.5, "soldRooms": 41, "totalRooms": 52 } },
"revenue": { "status": "fresh", "data": { "todayMinor": 1245000, "currency": "AFN", "ytdRevPar": "USD 38.40" } },
"housekeepingSummary": { "status": "fresh", "data": { "dirty": 18, "cleanInspected": 22, "outOfOrder": 2 } },
"maintenanceSummary": { "status": "fresh", "data": { "open": 4, "inProgress": 2, "blocked": 0 } },
"aiInbox": { "status": "fresh", "data": { "items": [/* 5 */], "total": 5 } },
"alerts": { "status": "fresh", "data": { "items": [/* 3 */], "critical": 0 } },
"syncStatus": { "status": "fresh", "data": { "deviceId": "dev_01H8Y...", "lastSyncAt": "2026-04-23T09:13:48Z", "pendingHints": [{"aggregate":"folio.charge","count":2,"oldestAgeSeconds":42}] } }
},
"staleness": {}
}

200 Response (partial)

When a widget times out:

{
"widgets": {
"ai": { "status": "unavailable" },
"alerts": { "status": "fresh", "data": { /* ... */ } },
/* ... */
},
"partial": true,
"staleness": { "ai": "unavailable" }
}

10. GET /today | /arrivals | /departures | /in-house

Identical envelope; data.items shape per route.

{
"id": "wbv_01H8Y...",
"view": "arrivals",
"composedAt": "2026-04-23T09:14:22Z",
"partial": false,
"data": {
"items": [
{
"reservationId": "rsv_01H8Y...",
"guestName": "Maryam K.", // truncated for telemetry safety
"roomTypeId": "rt_01H8Y...",
"roomNumber": "412",
"scheduledArrival": "2026-04-23T15:00:00Z",
"guestsCount": 2,
"loyaltyTier": "gold",
"specialRequests": ["high_floor","vegetarian_breakfast"],
"stayNights": 3,
"rateAmountMinor": 415000,
"currency": "AFN",
"balanceMinor": 0,
"checkedIn": false
}
// ...
],
"total": 12,
"nextPageToken": null
}
}

11. GET /housekeeping/board?propertyId=<id>

{
"id": "wbv_01H8Y...",
"view": "housekeeping_board",
"composedAt": "2026-04-23T09:14:22Z",
"data": {
"rooms": [
{
"roomId": "rm_01H8Y...",
"roomNumber": "412",
"status": "dirty",
"assignedTo": "opr_01H8Y...",
"estimatedReadyAt": "2026-04-23T11:00:00Z",
"lastUpdatedAt": "2026-04-23T09:00:00Z",
"linenChangeRequired": true,
"guestArriving": "2026-04-23T15:00:00Z"
}
],
"summary": { "dirty": 18, "cleanInspected": 22, "outOfOrder": 2 }
}
}

12. GET /maintenance/board?propertyId=<id>

{
"id": "wbv_01H8Y...",
"view": "maintenance_board",
"composedAt": "2026-04-23T09:14:22Z",
"data": {
"workOrders": [
{
"workOrderId": "wo_01H8Y...",
"title": "Leak in 412 bathroom",
"priority": "high",
"status": "in_progress",
"assignedTo": "opr_01H8Y...",
"openedAt": "2026-04-22T18:14:00Z",
"blocking": true,
"roomId": "rm_01H8Y..."
}
],
"summary": { "open": 4, "inProgress": 2, "blocked": 0 }
}
}

13. GET /ai/suggestions?propertyId=<id>&category=<…>&limit=<n>

{
"items": [
{
"id": "aim_01H8Y...",
"suggestionId": "sg_01H8Y...",
"category": "housekeeping_reorder",
"severity": "attention",
"createdAt": "2026-04-23T08:00:00Z",
"expiresAt": "2026-04-23T18:00:00Z",
"summary": "Reorder housekeeping queue: prioritize rooms 412, 415, 410 (arrivals 15:00).",
"rationaleHint": "Estimated overlap with arrival window > 60 min if current order kept.",
"actionTemplate": {
"kind": "housekeeping.reorder",
"diff": [{ "roomId":"rm_412", "fromPosition": 7, "toPosition": 1 }]
},
"provenance": {
"model": "ghasi-ops-v3",
"modelVersion": "3.2.1",
"promptVersion": "hk-reorder-v5",
"modelClass": "cloud",
"signatureFingerprint": "sha256:..."
}
}
],
"total": 5,
"nextPageToken": null
}

14. POST /ai/suggestions/{id}/decide

Request

POST /api/bff/backoffice/v1/ai/suggestions/aim_01H8Y.../decide
Authorization: Bearer ...
DPoP: ...
X-Idempotency-Key: idem_01H8Y...
X-Device-Id: dev_01H8Y...
Content-Type: application/json

{
"outcome": "accepted",
"modifiedDelta": null,
"notes": "Looks right; applying."
}

200 Response

{
"id": "aim_01H8Y...",
"decision": { "outcome": "accepted", "decidedBy": "opr_01H8Y...", "decidedDeviceId": "dev_01H8Y...", "decidedAt": "2026-04-23T09:14:22Z" }
}

Errors

404 SUGGESTION_NOT_FOUND, 409 DECISION_ALREADY_RECORDED, 412 PRECONDITION_FAILED.

15. GET /alerts?propertyId=<id>&filter=<unack|all|critical>

{
"items": [
{
"id": "ali_01H8Y...",
"alertId": "alt_01H8Y...",
"category": "lock_failed",
"severity": "critical",
"raisedAt": "2026-04-23T08:42:14Z",
"resolvedAt": null,
"acknowledgedAt": null,
"payload": { "reservationId": "rsv_01H8Y...", "vendor":"ttlock","reason":"BLE timeout" }
}
],
"summary": { "critical": 1, "warning": 0, "info": 2 }
}

16. POST /alerts/{id}/acknowledge

POST /api/bff/backoffice/v1/alerts/ali_01H8Y.../acknowledge
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json

{ "notes": "Investigating with on-site engineer." }

200 returns updated AlertInboxEntry. Errors include 409 ALERT_ALREADY_ACKED.

17. PUT /preferences

Optimistic concurrency.

Request

PUT /api/bff/backoffice/v1/preferences
X-Idempotency-Key: idem_01H8Y...
If-Match: "version-7"
Content-Type: application/json

{
"layout": { "density": "compact", "columnsByPage": { "arrivals": ["guestName","room","arrival","balance"] } },
"notifications": { "sound": true, "popup": true, "aiBadge": true },
"i18n": { "locale": "ps-AF", "firstDayOfWeek": "sat" },
"appearance": { "theme": "dark" }
}

200 Response

Returns updated OperatorPreferences with new version.

Errors

412 PREFERENCES_CONFLICT when If-Match mismatch.

18. POST /devices/{deviceId}/heartbeat

Sent every 60 s by the desktop main process.

{
"occurredAt": "2026-04-23T09:14:22Z",
"appVersion": "1.4.2",
"appPlatform": "win32",
"networkProfile": "good",
"currentPropertyId": "prop_01H8Y...",
"outboxHints": [
{ "aggregate": "folio.charge", "count": 2, "oldestAgeSeconds": 42 },
{ "aggregate": "housekeeping.task.transition", "count": 0, "oldestAgeSeconds": 0 }
],
"memoryRssMb": 412,
"cpuPctOneMin": 6.4,
"lastSyncCursor": "ck_v1_..."
}

204 No Content. Errors: 403 DEVICE_MISMATCH, 429 RATE_LIMITED (one per minute).

19. POST /sync/handshake

Negotiate a sync session; thin proxy to sync-service.handshake.

Request

{
"deviceId": "dev_01H8Y...",
"appVersion": "1.4.2",
"capabilities": ["bulk-pull-v1","push-batched-v1","resumable-v1"],
"lastKnownCursor": "ck_v1_..."
}

200 Response

{
"syncSessionToken": "sst_...",
"expiresAt": "2026-04-23T09:44:22Z",
"pullEndpoint": "https://sync.melmastoon.ghasi.io/sync/v1/pull",
"pushEndpoint": "https://sync.melmastoon.ghasi.io/sync/v1/push",
"cursor": "ck_v1_...",
"maxBatchSize": 500,
"compressionAccepted": ["zstd"]
}

After this call, the desktop talks directly to sync-service. The BFF is not on the bulk transfer path. See SYNC_CONTRACT.

20. GET /sync/cursor?deviceId=<id>

{
"deviceId": "dev_01H8Y...",
"lastCursor": "ck_v1_...",
"lastSyncHandshakeAt": "2026-04-23T09:00:01Z",
"pendingHints": [{"aggregate":"folio.charge","count":2,"oldestAgeSeconds":42}],
"status": "online"
}

21. POST /sync/cursor

Notify BFF of cursor advance after a successful pull/push.

{
"deviceId": "dev_01H8Y...",
"cursor": "ck_v1_...",
"advancedAt": "2026-04-23T09:14:22Z",
"advanceKind": "pull|push|both"
}

204. Emits melmastoon.bff.backoffice.sync.cursor_advanced.v1.

22. POST /locks/{reservationId}/issue-key

Request

POST /api/bff/backoffice/v1/locks/rsv_01H8Y.../issue-key
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json

{
"vendor": "ttlock",
"encoderId": "enc_lobby_01",
"validFrom": "2026-04-23T15:00:00Z",
"validUntil": "2026-04-26T11:00:00Z",
"scopes": ["room:412"],
"issueChannel": "front_desk_encoder",
"mfaAttestationToken": null
}

200 Response

{
"keyCredentialId": "kc_01H8Y...",
"vendor": "ttlock",
"encoderResponse": { "writeOk": true, "encoderToken": "enc_..." },
"issuedAt": "2026-04-23T09:14:22Z",
"validFrom": "2026-04-23T15:00:00Z",
"validUntil": "2026-04-26T11:00:00Z"
}

Errors

401 MFA_REQUIRED (per tenant policy), 403 PROPERTY_OUT_OF_SCOPE, 409 RESERVATION.OVERBOOKING_BLOCKED, 502 LOCK.VENDOR_UNAVAILABLE, 504 UPSTREAM_TIMEOUT.

23. POST /locks/{reservationId}/revoke-key

Always requires MFA step-up (/auth/mfa/step-up first).

POST /api/bff/backoffice/v1/locks/rsv_01H8Y.../revoke-key
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json

{
"keyCredentialId": "kc_01H8Y...",
"reason": "guest_lost_key|guest_compromised|early_departure|other",
"notes": "Guest reported lost key at front desk.",
"mfaAttestationToken": "mfa_01H8Y..."
}

200 returns revocation status; 401 MFA_INVALID_OR_USED if attestation is bad.

24. POST /reservations/{id}/check-in

Generic mutation proxy. Body shape forwarded to reservation-service.checkIn plus audit envelope.

POST /api/bff/backoffice/v1/reservations/rsv_01H8Y.../check-in
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json

{
"actualArrivalAt": "2026-04-23T15:42:00Z",
"roomNumberAssigned": "412",
"guestIdScannedRefs": ["doc_01H8Y..."],
"preAuthMinor": 15000,
"currency": "AFN",
"specialNotes": "Guest requested early breakfast tray."
}

200 returns the checked-in reservation snapshot from the reservation service. Errors are passed through unmodified except for envelope normalization.

25. POST /folios/{id}/charges

POST /api/bff/backoffice/v1/folios/fol_01H8Y.../charges
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json

{
"kind": "incidental",
"description": "Mini-bar — 2 sodas, 1 chocolate",
"amountMinor": 32000,
"currency": "AFN",
"category": "fnb",
"taxProfileRef": "tax_default_2026"
}

26. POST /housekeeping/tasks/{id}/transition

POST /api/bff/backoffice/v1/housekeeping/tasks/hkt_01H8Y.../transition
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json

{
"to": "clean_inspected",
"inspectorOperatorId": "opr_01H8Y...",
"linenLogRef": "lin_01H8Y..."
}

27. POST /maintenance/work-orders/{id}/transition

POST /api/bff/backoffice/v1/maintenance/work-orders/wo_01H8Y.../transition
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json

{
"to": "completed",
"resolution": "replaced_p_trap",
"partsUsed": [{ "sku":"P-TRAP-50", "qty": 1 }],
"actualCostMinor": 50000,
"currency": "AFN"
}

28. POST /auth/mfa/step-up

Exchanges a primary auth + a fresh MFA factor for an MfaAttestationToken.

{
"scope": "lock_revoke",
"factor": "totp",
"code": "123456"
}

200 returns:

{
"attestationToken": "mfa_01H8Y...",
"expiresAt": "2026-04-23T09:19:22Z",
"scope": "lock_revoke"
}

29. GET /sse/stream?propertyId=<id>&channels=ai,alerts,dashboard-refresh-hints,session

Server-Sent Events. Per-device single connection; multiplex.

Frame example

event: ai
id: evt_01H8Y...
data: {"kind":"new","payload":{ /* AISuggestionInboxEntry */ }}

event: alerts
id: evt_01H8Y...
data: {"kind":"new","payload":{ /* AlertInboxEntry */ }}

event: dashboard-refresh-hints
id: evt_01H8Y...
data: {"propertyId":"prop_01H8Y...","reason":"reservation.checked_in","invalidate":["dashboard","arrivals","in-house"]}

event: session
id: evt_01H8Y...
data: {"kind":"forceLogout","reason":"iam.session.revoked"}

If SSE not available (proxy strips it), the desktop falls back to polling:

  • GET /ai/suggestions every 60 s
  • GET /alerts every 30 s
  • GET /dashboard re-render on each manual reload

The polling fallback is selectable via the X-Network-Profile: flaky header or the desktop's preferences.

30. GET /health/live and GET /health/ready

PathPurpose
/health/live200 if process running
/health/ready200 if Memorystore + Postgres + 80% of upstream circuits closed

Open paths; never carry auth.

31. Rate limits

ScopeLimitNotes
Per device per endpoint family60 / min for reads, 30 / min for mutationsToken bucket
/devices/*/heartbeat1 / minStrictly enforced
/auth/refresh5 / 15 min / deviceAnti-brute-force
/locks/*30 / hour / deviceSensitive
/sse/stream1 active conn / deviceNew conn closes prior

429 carries Retry-After.

32. Pagination

Cursor-based for any list endpoint:

GET /alerts?cursor=<opaque>&limit=50

Response carries nextPageToken. Limit max 100.

33. CORS

Requests originate from app://melmastoon-backoffice (Electron custom URL scheme), not browser origins. CORS allow-list includes only:

  • app://melmastoon-backoffice
  • https://stage-backoffice-debug.melmastoon.ghasi.io (debug build)
  • http://localhost:5173 (dev renderer)

All others 403.

34. Caching policy

EndpointServer cacheClient cache
/dashboard30 s Memorystorenone (always re-fetch on focus)
`/today/arrivals/departures
`/housekeeping/board/maintenance/board`15 s Memorystore
/ai/suggestionsupstream-boundednone
/alertsupstream-boundednone
/preferences5 min Memorystoredesktop owns local mirror; refresh on app focus
/sync/cursor30 s Memorystoredesktop reads on reconnect

35. Compatibility — Electron contextBridge mapping

The desktop maps these endpoints behind window.melmastoon.*:

EndpointBridge call
GET /dashboardwindow.melmastoon.dashboard.fetch({propertyId})
GET /todaywindow.melmastoon.workbench.today({propertyId})
POST /reservations/{id}/check-inwindow.melmastoon.reservations.checkIn(id, body)
POST /locks/{id}/issue-keywindow.melmastoon.locks.issueKey(rsv, body)
POST /sync/handshakewindow.melmastoon.sync.handshake(input)
GET /sse/streamwindow.melmastoon.sse.subscribe(channels, onEvent)
POST /devices/{id}/heartbeatwindow.melmastoon.telemetry.heartbeat(payload)
POST /ai/suggestions/{id}/decidewindow.melmastoon.ai.decide(id, decision)

The bridge layer in the Electron main process attaches Authorization, DPoP, X-Device-Id, X-App-Version, X-App-Platform, X-Network-Profile, and X-Idempotency-Key automatically.

36. OpenAPI

openapi/openapi.yaml is generated from controller decorators. CI fails if the generated file diverges from the committed one without a corresponding PR.