Canonical Error Codes
Machine-readable, stable, string-valued. Use these exact codes in the error.code field of API and sync error responses (see docs/05-api-design.md).
Never invent new codes in a PR without adding them here.
Format
MELMASTOON.<DOMAIN>.<CODE> — three uppercase, dot-separated segments.
<DOMAIN>— the bounded context (e.g.,RESERVATION,LOCK,BILLING).<CODE>—SCREAMING_SNAKE_CASEdescribing the failure.
Error response template
{
"error": {
"type": "https://errors.melmastoon.ghasi.io/<domain>/<code>",
"code": "MELMASTOON.<DOMAIN>.<CODE>",
"title": "<short human summary>",
"status": <http status>,
"detail": "<specific explanation, safe to show to user>",
"instance": "<request path>",
"errors": [ { "field": "<optional field>", "code": "<optional sub-code>" } ],
"traceId": "<W3C trace id>",
"requestId": "<ULID>",
"tenantId": "<tnt_...>",
"retriable": <bool>,
"retryAfter": <seconds or null>,
"userMessageKey": "errors.<domain>.<code>",
"docUrl": "https://docs.melmastoon.ghasi.io/errors/<domain>/<code>",
"runbook": "https://runbooks.melmastoon.ghasi.io/<domain>/<code>"
}
}
userMessageKey resolves through the i18n bundle (@ghasi/ui-melmastoon/i18n) so that consumer, tenant booking, and backoffice surfaces show locale-appropriate Pashto / Dari / Arabic / EN / FR text.
Universal rules
- Never reuse a code across domains.
- Never change the HTTP status for an existing code.
- Never include stack traces or internal pointers in
detail. - Always set
traceId+requestId. - Always set
retriable+retryAfterwhere a retry is sensible. - Never expose whether a resource exists across tenants — return
MELMASTOON.GENERAL.RESOURCE_NOT_FOUND, not a 403, for cross-tenant access attempts.
GENERAL
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.GENERAL.RESOURCE_NOT_FOUND | 404 | no | errors.general.resource_not_found | Resource does not exist or is hidden by RLS / tenant scope. | general/resource-not-found |
MELMASTOON.GENERAL.VALIDATION_FAILED | 422 | no | errors.general.validation_failed | One or more request fields failed schema or business validation; see errors[]. | general/validation |
MELMASTOON.GENERAL.PRECONDITION_FAILED | 412 | no | errors.general.precondition_failed | If-Match version mismatch on optimistic-concurrency write. | general/precondition |
MELMASTOON.GENERAL.CROSS_TENANT_REFERENCE | 422 | no | errors.general.cross_tenant_reference | Aggregate construction or repository call referenced an entity from a different tenant. | general/cross-tenant |
MELMASTOON.GENERAL.RATE_LIMITED | 429 | yes | errors.general.rate_limited | Tenant or IP exceeded the rate-limit bucket. | general/rate-limit |
MELMASTOON.GENERAL.INTERNAL | 500 | yes | errors.general.internal | Uncaught error; alert paged. Check trace. | general/internal |
IDENTITY
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.IDENTITY.INVALID_CREDENTIALS | 401 | no | errors.identity.invalid_credentials | Username/password incorrect or account locked. | identity/invalid-credentials |
MELMASTOON.IDENTITY.TOKEN_EXPIRED | 401 | no | errors.identity.token_expired | Access token expired; refresh or re-login. | identity/token-expired |
MELMASTOON.IDENTITY.MFA_REQUIRED | 401 | no | errors.identity.mfa_required | Step-up MFA needed for this operation. | identity/mfa |
MELMASTOON.IDENTITY.DEVICE_NOT_BOUND | 403 | no | errors.identity.device_not_bound | Desktop request from an unbound device; complete device pairing. | identity/device-binding |
MELMASTOON.IDENTITY.PERMISSION_DENIED | 403 | no | errors.identity.permission_denied | RBAC/ABAC denied this action for the caller's role. | identity/permission |
MELMASTOON.IDENTITY.SESSION_REVOKED | 401 | no | errors.identity.session_revoked | Session was revoked (admin action or device unbinding). | identity/session-revoked |
TENANT
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.TENANT.NOT_FOUND | 404 | no | errors.tenant.not_found | X-Tenant-Id header references unknown tenant. | tenant/not-found |
MELMASTOON.TENANT.SUSPENDED | 403 | no | errors.tenant.suspended | Tenant suspended for billing or compliance. | tenant/suspended |
MELMASTOON.TENANT.NOT_A_MEMBER | 403 | no | errors.tenant.not_a_member | Authenticated user is not a member of this tenant. | tenant/not-a-member |
MELMASTOON.TENANT.PLAN_LIMIT_EXCEEDED | 402 | no | errors.tenant.plan_limit_exceeded | Action would exceed plan limits (rooms, properties, users). | tenant/plan-limits |
MELMASTOON.TENANT.CONFIGURATION_INVALID | 422 | no | errors.tenant.configuration_invalid | Tenant configuration (currency, locale, tax rules) failed validation. | tenant/config |
PROPERTY
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.PROPERTY.NOT_FOUND | 404 | no | errors.property.not_found | Property ID unknown for this tenant. | property/not-found |
MELMASTOON.PROPERTY.INACTIVE | 409 | no | errors.property.inactive | Property exists but is inactive (closed for season / suspended). | property/inactive |
MELMASTOON.PROPERTY.ROOM_TYPE_INVALID | 422 | no | errors.property.room_type_invalid | RoomType not configured or capacity invalid. | property/room-type |
MELMASTOON.PROPERTY.ROOM_NOT_AVAILABLE | 409 | no | errors.property.room_not_available | Specific room is OOO/OOS (out-of-order / out-of-service). | property/room-status |
MELMASTOON.PROPERTY.GEO_INVALID | 422 | no | errors.property.geo_invalid | Latitude/longitude or address failed validation. | property/geo |
MELMASTOON.PROPERTY.AMENITY_UNKNOWN | 422 | no | errors.property.amenity_unknown | Amenity code not in canonical list. | property/amenity |
RESERVATION
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED | 409 | no | errors.reservation.overbooking_blocked | Confirming this booking would exceed allocation; another reservation took the last unit. | reservation/overbooking |
MELMASTOON.RESERVATION.HOLD_EXPIRED | 410 | no | errors.reservation.hold_expired | Inventory hold expired before the consumer paid. | reservation/hold-expired |
MELMASTOON.RESERVATION.INVALID_STATE_TRANSITION | 409 | no | errors.reservation.invalid_state_transition | Cannot transition reservation in its current state (e.g., check-out an unconfirmed booking). | reservation/state-machine |
MELMASTOON.RESERVATION.GUEST_INFO_INCOMPLETE | 422 | no | errors.reservation.guest_info_incomplete | Required guest fields (name, ID document, contact) missing. | reservation/guest-info |
MELMASTOON.RESERVATION.CHECKIN_WINDOW_VIOLATED | 409 | no | errors.reservation.checkin_window_violated | Check-in attempted outside the configured window without an override. | reservation/checkin-window |
MELMASTOON.RESERVATION.CANCELLATION_NOT_ALLOWED | 409 | no | errors.reservation.cancellation_not_allowed | Cancellation policy forbids this action (e.g., non-refundable rate past cutoff). | reservation/cancellation-policy |
INVENTORY
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.INVENTORY.ALLOCATION_NOT_FOUND | 404 | no | errors.inventory.allocation_not_found | Allocation for the given (property, roomType, date) does not exist. | inventory/allocation-not-found |
MELMASTOON.INVENTORY.INSUFFICIENT_AVAILABILITY | 409 | no | errors.inventory.insufficient_availability | Not enough units available for the requested date range. | inventory/insufficient |
MELMASTOON.INVENTORY.STOP_SELL_ACTIVE | 409 | no | errors.inventory.stop_sell_active | A stop-sell is active for this date / room type. | inventory/stop-sell |
MELMASTOON.INVENTORY.MIN_LOS_VIOLATED | 422 | no | errors.inventory.min_los_violated | Length of stay below the configured minimum. | inventory/min-los |
MELMASTOON.INVENTORY.MAX_LOS_VIOLATED | 422 | no | errors.inventory.max_los_violated | Length of stay above the configured maximum. | inventory/max-los |
MELMASTOON.INVENTORY.OVERSELL_DETECTED | 500 | no | errors.inventory.oversell_detected | Internal invariant violation: more confirmed reservations than allocation. Page on-call. | inventory/oversell |
PRICING
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.PRICING.RATE_PLAN_NOT_FOUND | 404 | no | errors.pricing.rate_plan_not_found | RatePlan does not exist or is not active for the requested date. | pricing/rate-plan-not-found |
MELMASTOON.PRICING.RATE_PLAN_INACTIVE | 409 | no | errors.pricing.rate_plan_inactive | RatePlan is in draft or archived state. | pricing/inactive |
MELMASTOON.PRICING.CURRENCY_MISMATCH | 422 | no | errors.pricing.currency_mismatch | Quoted currency does not match property's billing currency. | pricing/currency |
MELMASTOON.PRICING.QUOTE_EXPIRED | 410 | no | errors.pricing.quote_expired | Price quote expired; re-quote required. | pricing/quote-expired |
MELMASTOON.PRICING.DERIVATION_FAILED | 500 | yes | errors.pricing.derivation_failed | Pricing engine failed to derive a final amount; check rules cascade. | pricing/derivation |
BILLING
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.BILLING.FOLIO_LOCKED | 409 | no | errors.billing.folio_locked | Folio is closed; reopen via supervisor override. | billing/folio-locked |
MELMASTOON.BILLING.CHARGE_INVALID | 422 | no | errors.billing.charge_invalid | Charge amount, currency, or category invalid. | billing/charge |
MELMASTOON.BILLING.INVOICE_ALREADY_ISSUED | 409 | no | errors.billing.invoice_already_issued | Invoice already issued for this folio period; void before re-issue. | billing/invoice-issued |
MELMASTOON.BILLING.TAX_RULE_MISSING | 422 | no | errors.billing.tax_rule_missing | No applicable tax rule for jurisdiction × date × charge category. | billing/tax-rule |
MELMASTOON.BILLING.REFUND_EXCEEDS_BALANCE | 422 | no | errors.billing.refund_exceeds_balance | Refund amount exceeds folio balance. | billing/refund-balance |
MELMASTOON.BILLING.RECONCILIATION_MISMATCH | 500 | no | errors.billing.reconciliation_mismatch | Folio totals disagree with the underlying ledger; page finance on-call. | billing/reconciliation |
PAYMENT
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.PAYMENT.GATEWAY_TIMEOUT | 504 | yes | errors.payment.gateway_timeout | Upstream gateway (PayPal / card processor / MFS) timed out. | payment/gateway-timeout |
MELMASTOON.PAYMENT.DECLINED | 402 | no | errors.payment.declined | Issuer declined the payment instrument. | payment/declined |
MELMASTOON.PAYMENT.INSUFFICIENT_FUNDS | 402 | no | errors.payment.insufficient_funds | Insufficient funds on the instrument. | payment/insufficient-funds |
MELMASTOON.PAYMENT.INTENT_NOT_FOUND | 404 | no | errors.payment.intent_not_found | PaymentIntent ID unknown or expired. | payment/intent-not-found |
MELMASTOON.PAYMENT.WEBHOOK_SIGNATURE_INVALID | 401 | no | errors.payment.webhook_signature_invalid | Webhook signature mismatch — possible spoof or rotated secret. | payment/webhook-signature |
MELMASTOON.PAYMENT.CASH_RECONCILIATION_PENDING | 202 | yes | errors.payment.cash_reconciliation_pending | Cash-on-arrival not yet reconciled by front desk. | payment/cash-reconciliation |
HOUSEKEEPING
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.HOUSEKEEPING.TASK_NOT_FOUND | 404 | no | errors.housekeeping.task_not_found | Task ID unknown for this property. | housekeeping/task-not-found |
MELMASTOON.HOUSEKEEPING.ROOM_STATE_CONFLICT | 409 | no | errors.housekeeping.room_state_conflict | Room state changed between task assignment and update (e.g., guest checked back in). | housekeeping/room-state |
MELMASTOON.HOUSEKEEPING.STAFF_UNAVAILABLE | 409 | no | errors.housekeeping.staff_unavailable | Assigned staff member is off-shift. | housekeeping/staff-unavailable |
MELMASTOON.HOUSEKEEPING.TASK_ALREADY_COMPLETED | 409 | no | errors.housekeeping.task_already_completed | Task already marked completed. | housekeeping/already-completed |
MELMASTOON.HOUSEKEEPING.SCHEDULE_OVERFLOW | 422 | no | errors.housekeeping.schedule_overflow | Scheduling exceeds shift capacity. | housekeeping/schedule-overflow |
MAINTENANCE
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.MAINTENANCE.TICKET_NOT_FOUND | 404 | no | errors.maintenance.ticket_not_found | Maintenance ticket unknown. | maintenance/ticket-not-found |
MELMASTOON.MAINTENANCE.ROOM_BLOCKED | 409 | no | errors.maintenance.room_blocked | Room blocked by an open ticket; cannot sell until cleared. | maintenance/room-blocked |
MELMASTOON.MAINTENANCE.SEVERITY_INVALID | 422 | no | errors.maintenance.severity_invalid | Severity outside allowed enum. | maintenance/severity |
MELMASTOON.MAINTENANCE.ASSIGNMENT_FAILED | 409 | no | errors.maintenance.assignment_failed | No available technician matches the required skill. | maintenance/assignment |
NOTIFICATION
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.NOTIFICATION.TEMPLATE_NOT_FOUND | 404 | no | errors.notification.template_not_found | Template ID/locale combination not registered. | notification/template-not-found |
MELMASTOON.NOTIFICATION.CHANNEL_DISABLED | 409 | no | errors.notification.channel_disabled | Recipient opted out of this channel (email/SMS/push). | notification/channel-disabled |
MELMASTOON.NOTIFICATION.PROVIDER_UNAVAILABLE | 502 | yes | errors.notification.provider_unavailable | Upstream email/SMS/push provider failed. | notification/provider |
MELMASTOON.NOTIFICATION.RECIPIENT_INVALID | 422 | no | errors.notification.recipient_invalid | Recipient address/phone failed validation. | notification/recipient |
MELMASTOON.NOTIFICATION.RATE_LIMITED | 429 | yes | errors.notification.rate_limited | Per-recipient or per-tenant throttle reached. | notification/rate-limit |
LOCK
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.LOCK.VENDOR_UNREACHABLE | 502 | yes | errors.lock.vendor_unreachable | Lock vendor API unreachable (TTLock / Salto / Assa Abloy). Falls back to mechanical key flow. | lock/vendor-unreachable |
MELMASTOON.LOCK.KEY_ISSUE_FAILED | 502 | yes | errors.lock.key_issue_failed | Vendor returned an error issuing the key credential. | lock/key-issue |
MELMASTOON.LOCK.KEY_REVOKE_FAILED | 502 | yes | errors.lock.key_revoke_failed | Vendor returned an error revoking the key credential; security alert raised. | lock/key-revoke |
MELMASTOON.LOCK.DEVICE_NOT_PAIRED | 409 | no | errors.lock.device_not_paired | Lock device not paired to the property; complete pairing in backoffice. | lock/device-pairing |
MELMASTOON.LOCK.CREDENTIAL_EXPIRED | 410 | no | errors.lock.credential_expired | Key credential expired (date range past valid_until). | lock/credential-expired |
MELMASTOON.LOCK.CARD_ENCODER_OFFLINE | 503 | yes | errors.lock.card_encoder_offline | Front-desk card encoder is offline; queue for retry. | lock/encoder-offline |
SYNC
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.SYNC.CURSOR_OUT_OF_RANGE | 410 | no | errors.sync.cursor_out_of_range | Sync cursor too old; client must perform full rebase pull. | sync/cursor-out-of-range |
MELMASTOON.SYNC.MUTATION_REJECTED | 409 | no | errors.sync.mutation_rejected | Offline mutation violates a server-authoritative invariant. | sync/mutation-rejected |
MELMASTOON.SYNC.CONFLICT_DETECTED | 409 | no | errors.sync.conflict_detected | Conflict detected; resolution required per aggregate's policy. | sync/conflict |
MELMASTOON.SYNC.IDEMPOTENCY_KEY_REUSED | 409 | no | errors.sync.idempotency_key_reused | Same Idempotency-Key used with a different body. | sync/idempotency-reuse |
MELMASTOON.SYNC.DEVICE_UNBOUND | 403 | no | errors.sync.device_unbound | Device binding revoked; re-pair the desktop client. | sync/device-unbound |
MELMASTOON.SYNC.PAYLOAD_TOO_LARGE | 413 | no | errors.sync.payload_too_large | Push batch exceeds the per-call size budget. | sync/payload-too-large |
AI
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.AI.REFUSED_SAFETY | 422 | no | errors.ai.refused_safety | Pre-call or post-call safety classifier blocked the request. | ai/refused-safety |
MELMASTOON.AI.REFUSED_BUDGET | 429 | yes | errors.ai.refused_budget | Tenant or purpose AI budget exceeded for this period. | ai/refused-budget |
MELMASTOON.AI.PROVIDER_UNAVAILABLE | 502 | yes | errors.ai.provider_unavailable | Upstream LLM/embedding provider failed. | ai/provider-unavailable |
MELMASTOON.AI.HITL_REQUIRED | 403 | no | errors.ai.hitl_required | Action requires human-in-the-loop approval before commit. | ai/hitl-required |
MELMASTOON.AI.PROVENANCE_MISSING | 422 | no | errors.ai.provenance_missing | Persistence attempt for an AI artifact without AIProvenance VO. | ai/provenance-missing |
MELMASTOON.AI.OUTPUT_INVALID | 502 | yes | errors.ai.output_invalid | Structured output failed schema validation after repair attempt. | ai/output-invalid |
THEME
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.THEME.NOT_FOUND | 404 | no | errors.theme.not_found | Theme ID unknown for this tenant. | theme/not-found |
MELMASTOON.THEME.TOKEN_INVALID | 422 | no | errors.theme.token_invalid | Design token failed contrast or schema validation. | theme/token-invalid |
MELMASTOON.THEME.LAYOUT_PRESET_UNKNOWN | 422 | no | errors.theme.layout_preset_unknown | Layout preset not in canonical registry. | theme/layout-preset |
MELMASTOON.THEME.RTL_VARIANT_MISSING | 422 | no | errors.theme.rtl_variant_missing | RTL variant not provided for an asset that requires one. | theme/rtl-variant |
MELMASTOON.THEME.ASSET_TOO_LARGE | 413 | no | errors.theme.asset_too_large | Theme asset exceeds size budget (logo/hero/font). | theme/asset-size |
SEARCH
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.SEARCH.QUERY_INVALID | 422 | no | errors.search.query_invalid | Query string failed parsing or budget. | search/query-invalid |
MELMASTOON.SEARCH.GEO_OUT_OF_BOUNDS | 422 | no | errors.search.geo_out_of_bounds | Bounding box / radius outside supported limits. | search/geo-bounds |
MELMASTOON.SEARCH.INDEX_UNAVAILABLE | 503 | yes | errors.search.index_unavailable | Search index reindexing or degraded; serve cached results. | search/index-unavailable |
MELMASTOON.SEARCH.RANKER_UNAVAILABLE | 502 | yes | errors.search.ranker_unavailable | Personalization ranker offline; fallback to deterministic ranking. | search/ranker-unavailable |
MELMASTOON.SEARCH.PAGE_OUT_OF_RANGE | 422 | no | errors.search.page_out_of_range | Cursor refers to an expired or invalid page. | search/page |
BFF
| Code | HTTP | Retriable | i18n key | Root-cause hint | Runbook |
|---|---|---|---|---|---|
MELMASTOON.BFF.UPSTREAM_TIMEOUT | 504 | yes | errors.bff.upstream_timeout | A downstream service did not respond within the BFF deadline. | bff/upstream-timeout |
MELMASTOON.BFF.UPSTREAM_UNAVAILABLE | 502 | yes | errors.bff.upstream_unavailable | A downstream service is down; partial response served if possible. | bff/upstream-unavailable |
MELMASTOON.BFF.AGGREGATE_FAILED | 500 | yes | errors.bff.aggregate_failed | One or more parallel calls failed and the aggregate cannot be assembled. | bff/aggregate-failed |
MELMASTOON.BFF.SURFACE_MISMATCH | 403 | no | errors.bff.surface_mismatch | Caller surface (consumer / tenant booking / backoffice) does not match this BFF. | bff/surface-mismatch |
MELMASTOON.BFF.SCHEMA_DRIFT | 502 | no | errors.bff.schema_drift | Downstream response did not match the BFF's expected schema; alert paged. | bff/schema-drift |