Error Codes
Every 4xx and 5xx response from the Media Agency API carries the
canonical JSON envelope with a stable machine-readable code. Clients
should branch on error.code (not on the English error.message or
the raw HTTP status).
Envelope shape
{
"error": {
"code": "listing_not_found",
"message": "No listing matches that identifier.",
"request_id": "req_01HX5Y7Z2M3N4P5Q6R7S8T9U0V",
"details": {
"listing_number": "ABC123"
}
}
}
code- short snake_case identifier; switch on this.message- human-readable one-liner; safe to surface to end users.request_id- mirrors theX-Request-Idresponse header; include it in every support ticket.details- optional structured context; empty object when not populated.
Codes
Authentication
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
auth.missing_key | 401 | No Authorization header, or malformed Bearer token format. | Send Authorization: Bearer ma_live_ak_<ULID>. Format is documented at /api-reference/authentication. |
auth.invalid_key | 401 | API key not recognised, revoked, or past expiry. | Rotate from Settings > API Keys. Deleted keys cannot be recovered; mint a fresh key and update your secrets store. |
auth.scope_denied | 403 | Key authenticated but lacks the scope for this endpoint. | Mint a new key with the required scope, or contact support if your plan does not include the requested surface. |
Idempotency
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
idempotency.missing | 400 | Mutating request without X-Idempotency-Key header. | Add X-Idempotency-Key: <UUIDv4 or ULID> to every POST and PATCH. Keys are scoped per-agency for 24 hours. |
idempotency_key_conflict | 409 | Same key reused with a different body within 24h. | Use a fresh key for the new body, or replay the exact original body to get the cached response back. |
idempotency_key_in_progress | 409 | Replay while the first call with this key is still in flight. | Wait for the first request to complete, then retry. The server serializes in-flight replays to avoid double-writes. |
Listing Lifecycle
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
listing_not_found | 404 | No listing matches that identifier (also used for cross-agency reads). | Verify the listing_number and confirm the API key belongs to the owning agency. Cross-agency reads always return 404. |
endpoint_not_in_scope | 404 | URL under /api/v1/listings/ that does not match a public endpoint. | Consult /api-reference/endpoints for the 10 supported routes. |
listing_not_activated | 409 | Status transition attempted before activation / credit capture. | Complete activation in the dashboard at /property/{listing_number}/edit before retrying the status PATCH. |
no_op_transition | 409 | new_status equals the current status; no change was performed. | Read the current status via GET /listings/{listing_number} before PATCHing. Duplicated transitions are refused loudly. |
outside_cancellation_window | 409 | Cancel attempted past the no-fee cutoff window. | Route the cancellation through the dashboard so a human reviewer can decide whether to waive the fee. |
listing_already_cancelled | 409 | Cancel attempted on a listing already in the cancelled state. | Second cancels are rejected to prevent duplicate webhook emission. Read the current status before retrying. |
section_not_exposed | 404 | PATCH targeted a section not exposed via the public API. | Only matterport and floorplan sections are mutable via the public API in v1. Other sections must go through the UI. |
invalid_section_payload | 422 | Section payload parsed but failed a higher-level semantic rule. | Inspect error.details for the offending field and reason. Common cause: operation='set' without a url field. |
Users
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
user_not_found | 404 | No user with that id (also used for cross-agency reads). | Verify the user_id and confirm the API key's agency owns that user. Cross-agency reads always return 404. |
Rate Limiting
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
rate_limit.exceeded | 429 | Per-key request budget exhausted for the current window. | Honour the Retry-After header with exponential jitter. See /api-reference/rate-limits for per-endpoint budgets. |
Validation
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
validation.failed | 422 | Request body failed Pydantic schema validation. | Consult error.details.fields for the per-field failure reasons and correct the payload before retrying. |
Placeholders
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
not_implemented | 501 | Endpoint reserved for a future wave; URL is live but body is not. | Check the changelog at /api-reference/changelog for the planned ship date of the section referenced in details. |
Server
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
server.internal | 500 | Unexpected server error. Includes request_id for support escalation. | Safe to retry with the SAME X-Idempotency-Key. If the error persists, file a support ticket quoting the request_id. |
Security non-negotiable: 404 hides 403
Cross-agency reads always return 404 listing_not_found or
404 user_not_found, never 403. Returning 403 would confirm that a
given id belongs to a different agency, enabling id enumeration across
the tenant boundary. This is deliberate; do not report it as a bug.
Source of truth
The canonical registry lives at
dashboard/fastapi_backend/app/api/v1/errors.py. This page is
auto-generated from that registry - do NOT hand-edit. Add a new code
to the Python module and re-run node docs/scripts/generate-all.mjs;
the CI drift-guard at .github/workflows/docs.yml blocks PRs whose
generated output does not match disk.