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 |
|---|---|---|---|
api_key_missing | 401 | No X-API-Key header on the request. | Send X-API-Key: sk_live_<token> (or sk_test_<token> for sandbox). Format is documented at https://docs.valara.cloud/api-reference/authentication. |
api_key_invalid | 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. |
auth_required | 401 | Endpoint accepts either an API key or a dashboard session cookie; neither was presented. | Send X-API-Key: sk_live_<token> for programmatic calls, or sign in at https://valara.cloud/login for the dashboard UI path. See https://docs.valara.cloud/api-reference/authentication. |
role_forbidden | 403 | Caller is authenticated but their role is not permitted on this endpoint (e.g. a viewer trying to upload media). | Contact the media agency owner to elevate the role, or call the endpoint with an API key that belongs to a permitted role. |
agency_resolution_failed | 500 | Caller's session is valid but the server could not resolve the user to a media_agency. Indicates a data integrity issue. | File a support ticket quoting the request_id; the agency membership record needs operator repair. |
api_key_header_mismatch | 401 | Caller sent Authorization: Bearer instead of X-API-Key. | Resend the same token under X-API-Key: sk_live_<token>. The Media Agency API uses a custom header, not Bearer auth. |
api_key_malformed_prefix | 401 | API key prefix is not sk_live_, sk_test_, or ma_live_. | Re-copy the full key from Settings > Developer. Check error.details.detected_prefix for what the server parsed. |
api_key_malformed_length | 401 | Prefix OK but total key length is outside allowed bounds. | The key looks truncated. Re-copy it from Settings > Developer and ensure no whitespace was lost. |
api_key_revoked | 401 | Key was revoked. Fires only on prove-of-presentation match. | This key cannot be reactivated. Mint a new key at Settings > Developer and update your secrets store. |
api_key_expired | 401 | Key is past its expires_at date. | Mint a fresh key at Settings > Developer. Expiry timestamp is echoed in error.details.expired_at. |
api_key_disabled | 401 | Key exists but is_active=false (administratively disabled). | Re-enable the key at Settings > Developer or contact the agency owner. Disabled keys can be re-enabled without rotation. |
api_key_wrong_mode | 401 | Sandbox key used on live endpoint, or live key on sandbox. | Use sk_test_<token> for sandbox calls and sk_live_<token> for live traffic. Modes are strictly enforced. |
cross_agency | 404 | Key authenticated but requested resource belongs to another agency. | Returned as 404 (not 403) to prevent cross-agency existence probing. Confirm the key's agency owns the resource. |
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 https://docs.valara.cloud/api-reference/endpoints for the 13 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. |
listing_number_taken | 409 | Create-listing attempted with a listing_number that already exists under the caller's agency. | Omit listing_number from the request body so the server mints a fresh one, or pick a different value matching [a-z0-9]{7}. |
agent_not_in_hierarchy | 422 | owner_user_id / agent_one_id / agent_two_id references a user that does not belong to the calling agency's hierarchy. | Confirm the user_id via GET /api/v1/users and verify the user rolls up to the caller's media_agency. Cross-agency agent assignments are rejected. |
Media
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
unsupported_mime_type | 422 | Uploaded content_type is not in the allowlist for this asset_type (e.g. image/webp for asset_type=video). | Pick a content_type from the asset-type-specific allowlist. Photos accept image/jpeg |
file_too_large | 422 | Declared size_bytes exceeds the asset-type's cap. The presigned URL would have been rejected at the bucket layer. | Check the per-asset-type size cap on the endpoint reference page, compress / transcode the asset, then retry with the new size_bytes value. |
celery_unavailable | 503 | The background delivery queue is temporarily unavailable so the upload pre-signer cannot hand off the post-upload task. | Retry after the Retry-After interval. If the error persists for more than 15 minutes, file a support ticket. |
Scheduling
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
calendar_not_connected | 403 | The agency has not connected a Google Calendar, so the availability endpoint has no source to read slots from. | Connect Google Calendar at /settings/integrations in the dashboard. The connection token is resolved up the manager chain, so any agency ancestor's connection also works. |
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 https://docs.valara.cloud/api-reference/rate-limits for per-endpoint budgets. |
rate_limit_misconfigured | 500 | The rate-limit middleware could not resolve the caller's identity or the endpoint's budget (internal config error). | File a support ticket quoting the request_id. Retries will keep hitting the same wall until an operator corrects the rate-limit configuration. |
rate_limit_unavailable | 503 | The rate-limit backend (Redis) is temporarily unreachable so the middleware fails closed instead of serving unmetered traffic. | Retry after a few seconds. If the error persists for more than 5 minutes, the platform is in a degraded state; check https://status.valara.cloud. |
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. |
validation_type_mismatch | 422 | Field value has the wrong JSON type. | Consult error.details.errors[].field for the path and coerce the value to the expected type before retrying. |
validation_missing_required | 422 | A required field is absent from the request body. | Include every field flagged in error.details.errors[]. See the endpoint schema in the OpenAPI docs. |
validation_unknown_field | 422 | Field sent that is not declared on the schema (extra=forbid). | Remove the offending field, or consult did_you_mean[] for the likely intended field name. |
validation_deprecated_field | 422 | Field matches a registered rename (e.g. old_name -> new_name). | Migrate to the new field name in error.details.errors[].migration.to. See the linked migration guide for payload examples. |
validation_out_of_range | 422 | Numeric value outside the schema's allowed bounds. | Inspect error.details.errors[].ctx for the bound violated and clamp the value before retrying. |
validation_enum_not_allowed | 422 | String value is not in the schema's allowed enum values. | Use one of the values in error.details.errors[].ctx.expected. Enum values are case-sensitive. |
validation_string_pattern | 422 | String failed a format constraint (regex, uuid, email, url, date). | Validate against the documented format. Check error.details.errors[].ctx.pattern for the regex if declared. |
validation_array_length | 422 | List is too short or too long for the schema. | Trim or pad per the bounds in error.details.errors[].ctx. Empty optional lists should be omitted, not sent as []. |
validation_nested_error | 422 | Wrapper code when a nested object has its own error list. | Drill into error.details.errors[].children[] for per-field reasons. Child entries carry their own codes. |
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 https://docs.valara.cloud/api-reference/changelog for the planned ship date of the section referenced in details. |
Server
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
internal_error | 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. |
Wire
| Code | HTTP | Summary | Remediation |
|---|---|---|---|
request_body_not_json | 400 | Content-Type is application/json but body does not parse. | Fix the JSON syntax. error.details.parse_error_position gives the approximate byte offset of the first parse failure. |
request_body_empty | 400 | Required request body is missing on a POST or PATCH. | Send a JSON object even when every field is optional. Empty bodies are rejected at the wire layer for safety. |
request_body_too_large | 413 | Request body exceeds the per-endpoint size limit. | Split the payload. Media uploads use the dedicated multipart endpoints; large batches use streaming. |
request_content_type_mismatch | 415 | Content-Type header does not match what the endpoint accepts. | Send Content-Type: application/json for body endpoints. Multipart endpoints document the type in OpenAPI. |
request_method_not_allowed | 405 | Path exists but the HTTP method is not supported. | Use one of the methods in error.details.methods_allowed (also echoed in the Allow response header). |
request_path_not_found | 404 | URL does not match any public API endpoint. | Consult https://docs.valara.cloud/api-reference/endpoints for the complete route catalog. |
request_query_invalid | 422 | Query-parameter validation failed. | Same shape as body validation: inspect error.details.errors[] for per-parameter reasons. Query keys are case-sensitive. |
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.