Error Code Reference¶
All Curaway API errors follow the naming convention {DOMAIN}_{CATEGORY}_{SEQUENCE} where:
- DOMAIN -- The system area (e.g.,
AUTH,INTAKE,FHIR) - CATEGORY -- The error type (e.g.,
TOKEN,VALIDATION,NOT_FOUND) - SEQUENCE -- A three-digit number (e.g.,
001,002)
Errors are returned inside the standard response envelope errors array.
AUTH -- Authentication & Authorization¶
Errors related to JWT validation, token lifecycle, and permission checks.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
AUTH_TOKEN_EXPIRED_001 |
401 | JWT token has expired | User session timed out | Re-authenticate via Clerk to obtain a fresh token |
AUTH_TOKEN_INVALID_001 |
401 | JWT signature verification failed | Tampered or malformed token | Ensure the token was issued by Clerk and has not been modified |
AUTH_UNAUTHORIZED_001 |
403 | User lacks permission for this action | Role does not have required scope | Check the user's role assignment in Clerk dashboard |
AUTH_HEADER_MISSING_001 |
401 | Authorization header not provided | Client did not send Bearer token | Add Authorization: Bearer <token> header to the request |
TENANT -- Tenant Management¶
Errors related to multi-tenant isolation and tenant configuration.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
TENANT_NOT_FOUND_001 |
404 | Tenant ID does not exist | Typo in X-Tenant-ID header |
Verify the tenant ID against the tenant registry |
TENANT_HEADER_MISSING_001 |
400 | X-Tenant-ID header not provided |
Client omitted the required header | Add X-Tenant-ID header to all API requests |
TENANT_MISMATCH_001 |
403 | JWT tenant claim does not match X-Tenant-ID header |
Cross-tenant access attempt | Ensure the JWT was issued for the same tenant specified in the header |
INTAKE -- Patient Intake & Case Management¶
Errors during patient creation, case lifecycle, and intake form processing.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
INTAKE_VALIDATION_001 |
422 | One or more input fields failed validation | Missing required fields or invalid format | Check the field property in the error object for the specific field |
INTAKE_DUPLICATE_001 |
409 | A patient with matching identifiers already exists | Same email + date of birth combination | Use the existing patient record or call the merge endpoint |
INTAKE_CASE_NOT_FOUND_001 |
404 | The specified case ID does not exist | Stale reference or typo | Query /cases/ to list available cases for the patient |
INTAKE_CASE_CLOSED_001 |
400 | Cannot modify a case that has been closed | Attempting to update a finalized case | Create a new case if further action is needed |
FHIR -- Clinical Data (FHIR R4)¶
Errors when reading or writing FHIR-formatted clinical resources.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
FHIR_RESOURCE_INVALID_001 |
422 | FHIR resource failed R4 schema validation | Missing required fields or invalid coding system | Validate the resource against the FHIR R4 spec before submission |
FHIR_RESOURCE_NOT_FOUND_001 |
404 | The requested FHIR resource does not exist | Invalid resource ID or resource was deleted | Verify the resource ID and type |
FHIR_CODING_UNKNOWN_001 |
422 | Unrecognized coding system or code value | Typo in ICD-10, SNOMED CT, or LOINC code | Cross-reference the code against the relevant coding system registry |
FHIR_CASE_ID_REQUIRED_001 |
400 | case_id query parameter is required on GET /api/v1/patients/{patient_id}/fhir (#1194 C7). #1194 C19: an explicit empty value (?case_id=) and whitespace-only values also trigger this code — the parameter must carry a non-empty UUID. |
Caller invoked the public FHIR list endpoint without scoping to a case (or with an empty value) — previously returned cross-case data, now rejected | Pass the case UUID as ?case_id=<uuid>; cross-case FHIR reads via this public endpoint are no longer supported |
FHIR_CASE_ID_INVALID_001 |
400 | case_id is present but is not a parseable UUID (#1194 C15). Applies to the public FHIR list endpoint and to the MCP get_patient_clinical_summary + run_match tool handlers. |
Typo, stale identifier, accidental concatenation, or a non-UUID stub left over from earlier development | Pass the case UUID exactly as returned by /api/v1/patients/{patient_id}/cases. Leading/trailing whitespace is stripped; malformed strings are rejected |
CASE_NOT_FOUND_001 |
404 | case_id is a valid UUID but no case exists with that ID (#1194 C16). Enforced on the public FHIR list endpoint and the two MCP tool handlers. |
Stale reference, deleted case, or a UUID that happens to parse but was never minted | Verify the case exists via /api/v1/patients/{patient_id}/cases; create a new case if the previous one was closed |
CASE_CROSS_TENANT_001 |
403 | case_id exists but belongs to a different tenant than the request's X-Tenant-ID (#1194 C16). Distinct from CASE_NOT_FOUND_001 to surface cross-tenant probes in audit logs. |
Copy/pasted case_id from another tenant's data; misconfigured X-Tenant-ID header |
Use case IDs owned by your tenant; double-check the X-Tenant-ID header matches the JWT's tenant claim |
CASE_WRONG_PATIENT_001 |
403 | case_id exists in the right tenant but is bound to a different patient than the route's {patient_id} / MCP patient_id argument (#1194 C16/C20). Without this check the response would be a silent empty 200, masking a within-tenant cross-patient leak surface. |
Caller authorised for patient A passed a case_id belonging to patient B (within the same tenant) | List the patient's cases via /api/v1/patients/{patient_id}/cases and pass a matching case_id |
MATCH -- Provider Matching¶
Errors from the AI-powered provider matching engine.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
MATCH_NO_RESULTS_001 |
404 | No providers matched the given criteria | Overly restrictive filters or unsupported procedure/country combination | Broaden search criteria or check that providers exist for the procedure |
MATCH_EMBEDDING_FAILED_001 |
500 | Failed to generate embedding for the search query | Embedding service (Qdrant/OpenAI) is unavailable | Retry after a brief delay; check embedding service health |
MATCH_GRAPH_QUERY_FAILED_001 |
500 | Neo4j graph traversal failed during matching | Neo4j connection timeout or corrupted graph data | Check Neo4j connectivity; re-seed graph if data is stale |
AGENT -- AI Agent & Chat¶
Errors from the conversational AI agent that guides patients through intake.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
AGENT_GUARDRAIL_INPUT_001 |
400 | Input was blocked by the guardrail classifier | User message contained prohibited content (PII solicitation, medical advice request) | Rephrase the message to stay within the agent's scope |
AGENT_GUARDRAIL_OUTPUT_001 |
500 | Agent response was blocked by output validator | LLM generated content matching a forbidden output pattern | Retry the request; if persistent, review guardrail rules |
AGENT_LLM_TIMEOUT_001 |
504 | LLM did not respond within the timeout window | High load on the LLM provider or network issues | Retry with exponential backoff; check LLM provider status |
AGENT_CONTEXT_OVERFLOW_001 |
400 | Conversation history exceeds the model's context window | Very long chat session without summarization | Start a new conversation or clear older messages |
PROVIDER -- Provider Management¶
Errors related to provider profiles and configuration.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
PROVIDER_NOT_FOUND_001 |
404 | The specified provider does not exist | Invalid provider ID | Query /providers/ to list available providers |
PROVIDER_INACTIVE_001 |
400 | Provider is currently inactive and cannot accept cases | Provider has been deactivated by admin | Contact the admin to reactivate the provider or choose a different one |
PROVIDER_CAPACITY_001 |
409 | Provider has reached their case capacity limit | Too many active cases assigned | Wait for existing cases to complete or select an alternative provider |
PROVIDER_NOT_FOUND |
404 | Admin: provider ID not found (cross-tenant admin scope) | Invalid or non-existent provider_id | Verify the provider_id via GET /api/v1/admin/providers |
PROVIDER_SLUG_TAKEN |
409 | Admin: the requested slug is already used by another provider | Duplicate slug on PATCH | Choose a unique slug or query existing providers |
DOCTOR -- Doctor Profiles¶
Errors related to doctor records, search, and procedure associations.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
DOCTOR_NOT_FOUND_001 |
404 | The specified doctor does not exist | Invalid doctor ID or doctor was removed | Query /doctors/ to list available doctors |
DOCTOR_VALIDATION_001 |
422 | Doctor profile data failed validation | Missing required fields (name, specialty, license number) | Check the field property in the error for specifics |
DOCTOR_DUPLICATE_001 |
409 | A doctor with the same license number already exists | Attempting to create a duplicate record | Use the existing doctor record or update it |
DOCTOR_PROCEDURE_EXISTS_001 |
409 | This doctor is already associated with the specified procedure | Duplicate procedure association attempt | No action needed -- the association already exists |
PUBLIC -- Public Storefront¶
Errors from the public-facing provider storefront (unauthenticated).
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
PUBLIC_PROVIDER_NOT_FOUND_001 |
404 | Provider slug does not exist | Invalid or stale provider URL | Verify the provider slug against the provider directory |
PUBLIC_DOCTOR_NOT_FOUND_002 |
404 | Doctor slug does not exist | Invalid or stale doctor URL | Verify the doctor slug against the doctor directory |
PUBLIC_TREATMENT_NOT_FOUND_003 |
404 | Treatment slug does not exist | Invalid or stale treatment URL | Verify the treatment slug against available treatments |
PUBLIC_DESTINATION_NOT_FOUND_004 |
404 | Country slug does not exist | Invalid or stale country URL | Verify the country slug against available destinations |
PUBLIC_INVALID_FILTER_005 |
400 | Invalid filter parameter value | Unrecognized or malformed filter value | Check the API docs for valid filter values |
PUBLIC_INVALID_SORT_006 |
400 | Invalid sort_by parameter | Unrecognized sort field | Valid sort fields: name, rating, price |
PUBLIC_SEARCH_QUERY_TOO_SHORT_007 |
400 | Search query less than 2 characters | User submitted a single character | Provide a search query of at least 2 characters |
PUBLIC_SEARCH_QUERY_TOO_LONG_008 |
400 | Search query exceeds 200 characters | Extremely long search string | Shorten the search query to 200 characters or fewer |
PUBLIC_RATE_LIMIT_EXCEEDED_009 |
429 | 60 requests per minute per IP exceeded | Automated scraping or high-frequency polling | Wait for the rate limit window to reset (1 minute) |
PUBLIC_CACHE_ERROR_010 |
500 | Redis cache failure (non-fatal, falls through to DB) | Redis connectivity issue | No user action needed -- request will be served from DB |
PUBLIC_PAGINATION_INVALID_011 |
400 | page < 1 or per_page > 100 | Invalid pagination parameters | Use page >= 1 and per_page between 1 and 100 |
PUBLIC_SLUG_CONFLICT_012 |
409 | Duplicate slug detected | Slug already exists for another entity | Choose a different slug or modify the existing one |
CONSENT -- GDPR Consent Management¶
Errors related to consent capture, revocation, and audit trails.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
CONSENT_NOT_FOUND_001 |
404 | No consent record found for this patient/purpose | Consent was never captured or was fully revoked | Prompt the patient to provide consent |
CONSENT_ALREADY_REVOKED_001 |
409 | Consent has already been revoked | Duplicate revocation attempt | No action needed -- consent is already revoked |
CONSENT_REQUIRED_001 |
403 | This action requires patient consent that has not been granted | Attempting a data-sharing action without consent | Capture consent from the patient before proceeding |
GRAPH -- Knowledge Graph (Neo4j)¶
Errors from the Neo4j-backed knowledge graph used for provider relationships and procedure hierarchies.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
GRAPH_CONNECTION_001 |
503 | Cannot connect to Neo4j | Neo4j instance is down or URI is misconfigured | Verify NEO4J_URI environment variable and Neo4j service health |
GRAPH_QUERY_FAILED_001 |
500 | Cypher query execution failed | Malformed query or constraint violation | Check the Cypher query syntax and data integrity |
GRAPH_REBUILD_UNAVAILABLE |
503 | Neo4j unavailable during graph rebuild | Neo4j instance is down when POST /admin/graph/rebuild was called |
Check Neo4j connectivity; rebuild will proceed without clearing the subgraph and logs a warning |
GRAPH_REBUILD_CONFIRM_REQUIRED |
400 | Rebuilding scope=all requires explicit confirmation |
POST /admin/graph/rebuild with scope=all was called without confirm: true in the body |
Add "confirm": true to the request body to acknowledge the full-graph clear |
TENANT -- Tenant Authorization¶
Errors related to cross-tenant admin operations.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
TENANT_OVERRIDE_REQUIRED |
403 | Operation requires explicit tenant:override permission |
Admin accessing cross-tenant data without the override permission | Ensure the actor holds tenant:override permission, or filter to their own tenant |
CACHE -- Redis Cache¶
Errors related to Redis caching operations (Upstash Redis).
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
CACHE_CONNECTION_001 |
503 | Cannot connect to Redis | Upstash Redis instance is down or URL is misconfigured | Verify UPSTASH_REDIS_URL and UPSTASH_REDIS_TOKEN environment variables |
CACHE_READ_FAILED_001 |
500 | Failed to read from cache | Key expired mid-operation or Redis timeout | Non-fatal -- request falls through to database. Check Redis health if persistent |
CACHE_WRITE_FAILED_001 |
500 | Failed to write to cache | Redis write quota exceeded or connection dropped | Check Upstash free tier limits (10K commands/day). Non-fatal for most operations |
CACHE_INVALIDATION_001 |
500 | Cache invalidation failed | Redis pipeline error during bulk key deletion | Stale data may be served until TTL expires. Retry or wait for natural expiry |
STREAMING -- SSE Streaming¶
Errors related to Server-Sent Event (SSE) streaming endpoints.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
STREAMING_CONNECTION_001 |
503 | Cannot establish SSE stream | Redis pub/sub unavailable or endpoint misconfigured | Verify Redis connectivity and retry connection |
STREAMING_TIMEOUT_001 |
504 | SSE stream exceeded maximum duration (5 minutes) | Long-running document processing or stalled pipeline | Reconnect and check document processing status via REST API |
STREAMING_PUBLISH_FAILED_001 |
500 | Failed to publish event to SSE channel | Redis RPUSH failed or channel not found | Non-fatal for document processing -- progress events are best-effort |
STREAMING_INVALID_CHANNEL_001 |
400 | Invalid or unauthorized SSE channel subscription | Patient ID does not belong to the authenticated tenant | Verify patient ownership and tenant isolation |
NOTIFY -- Notifications (Email, SMS)¶
Errors from the notification subsystem powered by Resend.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
NOTIFY_EMAIL_FAILED_001 |
500 | Failed to send email via Resend | Invalid recipient address or Resend API error | Verify the email address and Resend API key |
NOTIFY_TEMPLATE_NOT_FOUND_001 |
404 | Email template ID does not exist | Typo in template name or template was deleted | Check available templates in the notification config |
NOTIFY_RATE_LIMITED_001 |
429 | Too many notifications sent in the current window | Burst of notifications triggered by automation | Wait for the rate limit window to reset |
NOTIFY_RECIPIENT_INVALID_001 |
422 | Recipient address failed validation | Malformed email or phone number | Validate the recipient address format before sending |
VIDEO -- Video Consultation¶
Errors related to video consultation sessions.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
VIDEO_SESSION_NOT_FOUND_001 |
404 | The video session does not exist | Invalid session ID or session expired | Create a new video session |
VIDEO_SESSION_EXPIRED_001 |
410 | The video session has expired | Session exceeded its TTL | Create a new video session |
VIDEO_PROVIDER_ERROR_001 |
502 | Upstream video provider returned an error | Third-party video service outage | Retry after a delay; check provider status page |
VIDEO_PARTICIPANT_LIMIT_001 |
400 | Maximum number of participants reached | Exceeded the configured participant cap | Remove a participant or upgrade the session tier |
VIDEO_ROOM_PROVISION_FAILED_001 |
502 | Daily.co room creation failed | Daily.co API error or network timeout | Retry; if persistent check DAILY_API_KEY and Daily.co service status |
VIDEO_HIPAA_TIER_REQUIRED_001 |
503 | Non-HIPAA Daily.co tier in production with patient-bearing tenant | DAILY_HIPAA_TIER_ENABLED=false after mso_post_launch flag flipped |
Upgrade Daily.co plan to HIPAA tier and set DAILY_HIPAA_TIER_ENABLED=true |
VIDEO_PARTICIPANT_UNAUTHORIZED_001 |
403 | Caller is not a participant of this session | /join caller is not the doctor, patient, or companion on the session |
Verify the caller's identity matches a participant on the booking |
SCHED -- MSO Teleconsultation Scheduling¶
Errors from the MSO video consultation scheduling subsystem (ADR-0018 Phase 2a).
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
SCHED_SLOT_CONFLICT_001 |
409 | Doctor already has an active session at this slot | Overlapping scheduled_for window for the same doctor |
Choose a different time slot |
SCHED_OUT_OF_HOURS_001 |
422 | Slot is outside the doctor's configured working hours | scheduled_for outside 09:00–18:00 in tenant timezone |
Book within working hours (mso_consultation_hours_per_day setting) |
SCHED_TOO_LATE_001 |
422 | scheduled_for is less than 15 minutes from now |
Not enough time to provision the Daily.co room | Book at least 15 minutes in advance |
MSO -- MSO Payments¶
Errors from the MSO consultation payment subsystem (Stripe + Razorpay, ADR-0018 Phase 2a).
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
MSO_CHARGE_FAILED_001 |
402 | Payment provider authorization failed; booking aborted | Card declined or provider-side error | Use a different payment method; provider name included in error envelope |
MSO_PROVIDER_UNAVAILABLE_001 |
503 | Configured payments provider is unreachable | Stripe or Razorpay API is down | Retry with exponential backoff; check provider status page |
MSO_PROVIDER_NOT_CONFIGURED_001 |
503 | Selected provider has no credentials in env | RAZORPAY_KEY_ID or STRIPE_SECRET_KEY missing |
Operations team: add credentials to Railway env vars |
MSO_WEBHOOK_INVALID_SIGNATURE_001 |
400 | Webhook signature verification failed | Invalid Stripe-Signature or X-Razorpay-Signature |
Verify the webhook secret matches the provider dashboard |
MSO_WEBHOOK_DUPLICATE_001 |
200 | Duplicate webhook event detected (idempotency) | Provider retried a previously processed event | No action needed — event was already processed; 2xx stops provider retries |
MSO_SESSION_INVALID_TRANSITION_001 |
409 | Status transition is not valid for the session's current state | e.g. cancelling a completed session, ending an unstarted session, rescheduling a cancelled one | Check the current session status; only valid transitions are permitted per the state machine |
MSO_SESSION_ALREADY_CANCELLED_001 |
409 | Admin force-cancel rejected — session is already cancelled | Duplicate admin force-cancel request | No action needed — session is already in cancelled state; idempotency guard |
MSO_SESSION_NOT_FOUND_404 |
404 | Session does not exist or is inaccessible | Invalid session UUID or wrong tenant scope | Verify the session_id; super_admin may query cross-tenant |
STORAGE -- File Storage (Cloudflare R2)¶
Errors related to file uploads, presigned URLs, and document processing.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
STORAGE_UPLOAD_FAILED_001 |
500 | File upload to Cloudflare R2 failed | R2 connectivity issue or bucket misconfiguration | Check CLOUDFLARE_R2_* environment variables and R2 service status |
STORAGE_PRESIGN_FAILED_001 |
500 | Failed to generate presigned upload URL | Invalid R2 credentials or expired signing key | Rotate R2 credentials and verify configuration |
STORAGE_FILE_TOO_LARGE_001 |
413 | File exceeds the maximum allowed size | Upload exceeds the 20 MB limit | Compress the file or split into smaller documents |
STORAGE_FILE_TYPE_001 |
422 | File type is not allowed | Uploaded file extension is not in the allow list | Allowed types: PDF, JPEG, PNG, DICOM. Convert the file to an accepted format |
DOCUMENT_NOT_FOUND_001 |
404 | Document does not exist (or caller lacks access) | Wrong document_id, tenant mismatch, or non-owner caller |
Verify the document_id and that the authenticated user owns the patient that owns the document. Returned (instead of 403) to avoid leaking document existence. |
DOCUMENT_NOT_RETRIABLE_001 |
409 | Document analysis cannot be retried in its current state | Document is already completed or actively in_progress / processing |
No action needed — the document is either done or already running. Check the document's analysis_status field. |
DOCUMENT_RETRY_LIMIT_001 |
429 | Manual retry budget exhausted (max 3 per document) | Patient retried the document more than MAX_MANUAL_RETRIES times |
Contact support if the document still needs analysis — they can either bypass the rate limit or escalate to a clinical coordinator. |
DOCUMENT_RETRY_INTERNAL_001 |
500 | Internal error while initiating a document retry | Unexpected DB / dispatch error inside manual_retry (not the 409 / 429 contract paths). Surfaced with the standard envelope so FE A23 can render its error pill without special-casing raw 500s. |
Retry the request after a moment. If it persists, check the application logs for the underlying exception (Telegram WARNING document_retry_db is also fired). |
SYS -- System & Infrastructure¶
General system-level errors not tied to a specific domain.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
SYS_INTERNAL_001 |
500 | Unhandled internal server error | Uncaught exception in application code | Check server logs for the stack trace; report as a bug if persistent |
SYS_DATABASE_001 |
503 | PostgreSQL connection failed | Database is down or connection pool exhausted | Check DATABASE_URL configuration and database health |
SYS_RATE_LIMITED_001 |
429 | Global rate limit exceeded | Too many requests from this tenant | Wait for the rate limit window to reset; consider upgrading tier |
SYS_MAINTENANCE_001 |
503 | System is undergoing scheduled maintenance | Deployment or migration in progress | Retry after the maintenance window (check status page) |
SYS_TIMEOUT_001 |
504 | Request processing exceeded the timeout limit | Complex query or downstream service latency | Retry with a simpler request; if persistent, contact support |
SYS_CONFIG_MISSING |
500 | Required configuration value not set on the deployment | e.g. CLERK_SECRET_KEY absent at runtime |
Ops: set the env var and redeploy. Not retryable. |
FEATURE_DISABLED |
503 | Endpoint disabled by feature flag | Caller is gated by a Flagsmith flag that is off for this identity/tenant |
Wait for rollout, request identity-override, or enable the flag |
AUDIT_WRITE_FAILED |
500 | Database commit failed after a successful state change; the operation was rolled back | Audit Event INSERT raised SQLAlchemyError; transactional safety preserved | Retry is safe (no partial state); investigate DB if persistent |
FACILITATOR -- Facilitator Management¶
Errors related to facilitator profiles, attribution, and admin CRUD operations.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
FACILITATOR_NOT_FOUND |
404 | Facilitator ID does not exist | Invalid facilitator_id in URL or in patient registration | Verify the facilitator exists via GET /api/v1/admin/facilitators |
FACILITATOR_INACTIVE |
422 | Facilitator exists but is inactive (is_active=false) |
Attempting to assign an inactive facilitator during patient registration | Contact admin to reactivate the facilitator, or choose an active one |
FACILITATOR_DUPLICATE_EMAIL |
409 | Email address already in use by another active facilitator | Creating or updating facilitator with a duplicate email | Choose a different email or use the existing facilitator record |
FACILITATOR_HAS_ATTRIBUTED_RECORDS |
409 | Cannot delete facilitator — they have attributed patients/cases | Attempting DELETE without ?force=true when attributions exist |
Bulk-reassign attributed records to another facilitator first, or use ?force=true (requires admin:force permission, super_admin only) |
RBAC -- Role-Based Access Control¶
Errors emitted by the admin Users + Tenants endpoints (/api/v1/admin/users/*, /api/v1/admin/tenants/*) and any other RBAC-gated route.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
RBAC_INVALID_ROLE |
422 | Body role_code not present in the roles catalog |
Typo, removed role, or new role not yet seeded | Check the role code via /api/v1/admin/roles (when implemented) |
RBAC_INVALID_FILTER |
400 | Query-string filter is malformed or refers to an unknown value | e.g. ?role_code=… for a role that doesn't exist; path user_id doesn't match Clerk format |
Sanitize the filter and retry |
RBAC_LAST_ADMIN_GUARD |
409 | Revoking would leave 0 active holders of role_code on tenant_id |
Caller is the last admin and didn't pass force=true |
Pass ?force=true (requires admin:force permission, super_admin only) |
RBAC_FORCE_DENIED |
403 | force=true requested but caller lacks admin:force permission |
Only super_admin holds admin:force; platform_admin does not |
Have a super_admin perform the operation |
RBAC_DATA_INTEGRITY |
500 | A user_roles row references a role_id that no longer exists |
Data drift; role was hard-deleted while assignments still pointed at it | Investigate orphaned user_roles rows; restore the role row or hard-revoke the orphans |
AUTH_NO_ACTOR |
401 | Caller identity (request.state.user_id) missing on an authenticated route |
RBACMiddleware did not populate user_id (auth header missing/invalid) | Send a valid Authorization: Bearer <Clerk JWT> |
TENANT -- Admin Tenants page¶
Errors emitted by /api/v1/admin/tenants/* and /api/v1/admin/org-mappings/* (slice 2b).
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
TENANT_NOT_FOUND_001 |
404 | tenant_id not in the tenants table | Typo, deleted tenant | Verify against GET /api/v1/admin/tenants |
TENANT_ALREADY_EXISTS |
409 | Conflicting id or slug on create that doesn't match the existing row |
Idempotent create with mismatched fields, or duplicate slug across tenants | Use the existing row, or pick a different id/slug |
TENANT_INVALID_INPUT |
422 | Tenant id/slug/type/country_code shape rejected before DB write | Bad pattern (id must match ^[a-z][a-z0-9-]{1,35}$); country_code must be ISO 3166-1 alpha-3; tenant_type must be one of coordinator/mso/admin/facilitator |
Sanitize input |
TENANT_ACTIVE_CASES_GUARD |
409 | Deactivating a tenant with active (non-closed) cases | Tenant has open cases | Pass force=true (requires admin:force permission) |
TENANT_PROTECTED |
409 | Tenant is in PROTECTED_TENANT_IDS (today: tenant-curaway-admin) and cannot be deactivated by any actor — admin:force does NOT override |
Operator tried to deactivate tenant-curaway-admin | This is by design (spec decision 9). The protected tenant holds super_admin role assignments; deactivating would lock everyone out. |
ORG_MAPPING_NOT_FOUND |
404 | tenant_org_mappings row missing | Typo, already deleted | Verify via GET /api/v1/admin/org-mappings |
ORG_MAPPING_DUPLICATE |
409 | clerk_org_id already mapped to a tenant | Duplicate POST | Use existing mapping or DELETE the old one first |
CLERK -- Clerk integration¶
Errors raised by code that calls Clerk's admin API (e.g. email→user_id lookup).
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
CLERK_UNAVAILABLE |
503 | Clerk API call failed (network error or 5xx) | Transient outage or rate limit | Retry with backoff |
SYS_CONFIG_MISSING (above) covers the related "CLERK_SECRET_KEY not set" case; that is intentionally a separate code so retry/back-off does not mask config drift.
MSO Doctor Admin¶
Errors from the admin MSO doctor credentialing endpoints (/api/v1/admin/mso/doctors).
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
MSO_DOCTOR_NOT_FOUND_404 |
404 | Doctor not found in the specified tenant | Invalid doctor_id or cross-tenant access | Verify the doctor_id and X-Tenant-ID header |
MSO_DOCTOR_ALREADY_VERIFIED_409 |
409 | Doctor is already in verified status | Duplicate verify call | Check current credentialing_status before calling /verify |
MSO_DOCTOR_NOT_SUSPENDED_409 |
409 | Doctor is not in suspended status; cannot reinstate | Calling /reinstate on a doctor that is not suspended | Check current credentialing_status; only suspended doctors can be reinstated |
COMMERCE -- Commerce IntentService¶
Errors raised by app/services/commerce/intents.py (IntentService). Per spec §10.3 / §3.4 / §12 the provider HTTP call lives OUTSIDE the DB transaction and idempotency-key replay collapses duplicate create_intent calls into the same row without re-calling the provider.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
COMMERCE_INTENT_PROVIDER_DOWN_001 |
502 | Stripe / Razorpay returned an error or the SDK raised | Provider outage, transient 5xx, invalid provider response | Retry with the same (source_domain, source_id) — internal idempotency key replay protects against double-charge |
COMMERCE_INTENT_INVALID_AMOUNT_002 |
422 | amount_minor_units <= 0, > $100M cap, or currency not a 3-letter ISO 4217 code |
Caller passed bad inputs | Validate inputs at the router layer before invoking IntentService |
COMMERCE_INTENT_COMMISSION_UNRESOLVED_003 |
422 | No commission schedule matched (tenant / vendor / source_domain / currency) — fail-closed per spec §8 | Missing platform-default schedule or expired schedule | Seed the default commission schedule for the tenant; check effective_from/effective_to |
COMMERCE_INTENT_NOT_FOUND_004 |
404 | Intent not found within the calling tenant | Cross-tenant access attempt OR stale intent_id | Verify intent_id + X-Tenant-ID. Cross-tenant returns 404 (never 403) to avoid leaking existence |
COMMERCE_INTENT_INVALID_STATE_005 |
409 | capture / cancel called on an intent in an incompatible status |
e.g. cancelling an already-captured intent | Check intent.status; captured intents must be refunded via RefundService (PR-5b), not cancelled |
COMMERCE -- Commerce RefundService¶
Errors raised by app/services/commerce/refunds.py (RefundService). Per spec §10.3 the refund flow uses the strict two-transaction pattern: a pending row is inserted and committed BEFORE the provider HTTP call, then a second transaction commits success (status='succeeded' + ledger pair) or failure (status='failed' + error_log). Idempotency replay collapses duplicate calls onto the same row without re-calling the provider.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
COMMERCE_REFUND_PROVIDER_DOWN_001 |
502 | Stripe / Razorpay refund call failed or the SDK raised | Provider outage, transient 5xx, network error during the up-to-30s refund call | Pending row stays at status='failed' and an error_log row is written. Retry with the SAME caller_idempotency_key — the internal idempotency key replay returns the same row without double-charging |
COMMERCE_REFUND_OVER_REFUND_002 |
422 | amount_minor_units exceeds the remaining refundable balance (intent.amount - sum(prior_refunds)) |
Caller passed an amount larger than the captured-but-not-yet-refunded portion | Compute remaining balance client-side or pass the full remaining amount. Concurrent in-flight refunds reserve balance — wait for them to settle |
COMMERCE_REFUND_INTENT_NOT_CAPTURED_003 |
409 | Payment intent is not in a refundable status (must be captured, settled, or partially_refunded) |
Refund attempted on a pending / authorized / cancelled / failed intent, or on an intent that never reached the provider |
Use CancelIntent for unfinalized intents; check intent.status before calling RefundService |
COMMERCE_REFUND_INTENT_NOT_FOUND_004 |
404 | Payment intent not found within the calling tenant | Cross-tenant access attempt OR stale intent_id | Verify intent_id + X-Tenant-ID. Cross-tenant returns 404 (never 403) to avoid leaking existence |
COMMERCE_REFUND_INVALID_AMOUNT_005 |
422 | amount_minor_units <= 0 or not an integer |
Caller passed bad inputs | Validate inputs at the router layer before invoking RefundService |
COMMERCE -- Commerce Webhook Router¶
Errors raised by app/routers/commerce_webhooks.py. Spec §6. The endpoint at POST /api/v1/commerce/webhooks/{provider} is gated by feature flag commerce_webhooks_enabled (default OFF); legacy /api/v1/webhooks/{stripe,razorpay} remains authoritative until cutover.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
COMMERCE_WEBHOOK_DISABLED_001 |
503 | Feature flag commerce_webhooks_enabled is off |
Pre-cutover; legacy webhook routes are still authoritative | Provider will retry on its exponential schedule; flip the flag in both Production and Development to cut over |
COMMERCE_WEBHOOK_INVALID_SIGNATURE_001 |
400 | Signature header missing or HMAC mismatch | Wrong webhook secret, forge attempt, or tampered body | Verify the secret matches the provider dashboard; review alerting if rate is non-zero |
COMMERCE_WEBHOOK_EXPIRED_SIGNATURE_001 |
400 | Signed timestamp older than 300s (Stripe) or 7-day retention (Razorpay) | Replay attack OR severe clock skew between provider + server | Check NTP sync on Railway containers; investigate replay if rate is non-zero |
COMMERCE_WEBHOOK_PROVIDER_ERROR_001 |
503 | Provider adapter raised a non-signature error (SDK / configuration) | Missing env var, SDK version mismatch, malformed payload | Surface in logs; provider will retry. No double-process risk — dedup row not yet inserted |
COMMERCE_WEBHOOK_DUPLICATE_001 |
200 | (provider, provider_event_id) already present in webhook_events |
Provider retried a previously processed event | No action needed; 200 stops provider retries |
COMMERCE_WEBHOOK_NOT_IN_COMMERCE_001 |
200 | Event references a payment_intent / refund that does not exist in commerce tables | Legacy payment_intent (not migrated yet) or unknown id |
Legacy /api/v1/webhooks/{stripe,razorpay} handles it; commerce 200-acks per §6.3 |
COMMERCE_WEBHOOK_HANDLER_FAILED_001 |
500 | Canonical event dispatcher raised after signature passed | DB outage, repository error, downstream service down | A commerce_webhook_dlq row is written + provider will retry. After 5 retries the DLQ row is the only remaining trace — manual review via §11.8 |
COMMERCE -- Receipts¶
Errors raised by app/services/commerce/receipts.py:ReceiptService. Spec §4.7, §11. Receipts are generated for captured payment intents, stored in R2 with content-addressable keys, and emit a commerce_receipt_access_log row on every presigned URL issuance. Presigned URLs use a 15-minute TTL per spec §4.7.
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
COMMERCE_RECEIPT_INTENT_NOT_FOUND_001 |
404 | Payment intent not found within the calling tenant | Cross-tenant access attempt OR stale intent_id | Verify intent_id + X-Tenant-ID. Cross-tenant returns 404 (never 403) to avoid leaking existence |
COMMERCE_RECEIPT_INTENT_NOT_CAPTURED_002 |
409 | Payment intent is not in a receipt-eligible status (must be captured, settled, partially_refunded, or refunded) |
Receipt requested on a pending / authorized / cancelled / failed intent |
Capture the intent first via IntentService; receipts are only meaningful after settlement |
COMMERCE_RECEIPT_STORAGE_UNAVAILABLE_003 |
503 | R2 receipt bucket not configured or upload failed | Missing R2_* env vars, R2 outage, or bucket policy denial |
Verify R2 credentials + bucket policy; transient errors are retry-safe (content-addressable keys mean retries don't duplicate objects) |
COMMERCE_RECEIPT_NOT_FOUND_004 |
404 | Receipt not found within the calling tenant | Cross-tenant access attempt OR stale receipt_id | Verify receipt_id + X-Tenant-ID. Cross-tenant returns 404 (never 403) |
Commerce admin (PR-8)¶
| Code | HTTP Status | Description | Common Cause | Resolution |
|---|---|---|---|---|
COMMERCE_ADMIN_DISABLED_001 |
503 | Feature flag commerce_admin_ui_enabled is off |
Pre-cutover; admin commerce surface is dark | Flip commerce_admin_ui_enabled to true in BOTH Production and Development envs once the FE admin commerce screens ship |
COMMERCE_ADMIN_INTENT_NOT_FOUND_001 |
404 | Intent not found within the calling tenant (admin read) | Cross-tenant access attempt OR stale intent_id | Verify intent_id + X-Tenant-ID. Cross-tenant returns 404 (never 403) to avoid leaking existence |
COMMERCE_ADMIN_SCHEDULE_NOT_FOUND_001 |
404 | Commission schedule not found within the calling tenant | Cross-tenant access attempt OR stale schedule_id | Verify schedule_id + X-Tenant-ID |
COMMERCE_ADMIN_RECEIPT_NOT_FOUND_001 |
404 | Receipt not found within the calling tenant (admin read) | Cross-tenant access attempt OR stale receipt_id | Verify receipt_id + X-Tenant-ID. Admin reads also write an audit row to commerce_receipt_access_log |
COMMERCE_ADMIN_INVALID_DATE_001 |
422 | from_date / to_date query param is not an ISO-8601 timestamp |
Caller passed a non-ISO date string | Use YYYY-MM-DDTHH:MM:SSZ (or +00:00 suffix) for the date filters on /admin/commerce/* reads |
COMMERCE_WEBHOOK_LOG_NOT_FOUND_001 |
404 | Webhook log entry not found within the calling tenant | Cross-tenant access attempt OR stale event_log_id | Verify event_log_id + X-Tenant-ID. Cross-tenant returns 404 (never 403) |
COMMERCE_DLQ_NOT_FOUND_001 |
404 | DLQ entry not found (global table) | Stale dlq_id or entry already purged | Verify dlq_id via GET /admin/commerce/dlq list |
COMMERCE_DLQ_ALREADY_REPLAYED_002 |
409 | DLQ replay attempted with an idempotency key that was already used | Double-click or retry of a completed replay | The first replay succeeded; check the webhook log or commerce tables to confirm the event was processed |
Summary¶
| Domain | Code Count | Section |
|---|---|---|
| AUTH | 5 | Authentication |
| RBAC | 5 | RBAC |
| FACILITATOR | 4 | Facilitator |
| CLERK | 1 | Clerk |
| TENANT | 3 | Tenant |
| INTAKE | 4 | Intake |
| FHIR | 3 | FHIR |
| MATCH | 3 | Matching |
| AGENT | 4 | Agent |
| PROVIDER | 3 | Provider |
| DOCTOR | 4 | Doctor |
| PUBLIC | 12 | Public Storefront |
| CONSENT | 3 | Consent |
| GRAPH | 2 | Graph |
| CACHE | 4 | Cache |
| STREAMING | 4 | Streaming |
| NOTIFY | 4 | Notifications |
| VIDEO | 4 | Video |
| STORAGE | 4 | Storage |
| SYS | 5 | System |
| COMMERCE | 5 | Commerce IntentService |
| COMMERCE | 5 | Commerce RefundService |
| Total | 80 |
Procedure (admin onboarding)¶
Source: docs/specs/procedure-onboarding-admin-ui.md §4.5 / PR-1.
All codes returned by /api/v1/admin/procedures/*.
| Code | HTTP | Meaning |
|---|---|---|
PROCEDURE_ADMIN_DISABLED_001 |
503 | Feature flag procedure_admin_ui_enabled is off |
PROCEDURE_NOT_FOUND_404 |
404 | procedure_code not in the global catalog |
PROCEDURE_CODE_DUPLICATE_409 |
409 | Procedure code already exists |
PROCEDURE_HAS_ACTIVE_CASES_409 |
409 | Deactivate blocked by active cases (override with force=true + admin:force) |
PROCEDURE_INVALID_INPUT_422 |
422 | Validation failure (code shape, recovery days, etc.) |
PROVIDER_PROCEDURE_NOT_FOUND_404 |
404 | (provider, procedure) cell missing |
PROVIDER_PROCEDURE_INVALID_COST_RANGE_422 |
422 | min > avg or avg > max on cost cents |
Capability (admin surface)¶
Source: docs/specs/procedure-onboarding-admin-ui.md §9.2 / PR-2.
All codes returned by /api/v1/admin/capabilities, /api/v1/admin/providers/*/capabilities, /api/v1/admin/procedures/*/capability-requirements.
| Code | HTTP | Meaning |
|---|---|---|
CAPABILITY_NOT_FOUND_404 |
404 | Provider capability record not found |
CAPABILITY_DUPLICATE_409 |
409 | Capability already assigned to this provider |
CAPABILITY_INVALID_INPUT_422 |
422 | Invalid capability_code format or negative weight |
CAP_REQ_NOT_FOUND_404 |
404 | Procedure capability requirement not found |
CAP_REQ_DUPLICATE_409 |
409 | Capability requirement already exists for this procedure |
CAPABILITY_RECOMPUTE_FAILED_500 |
500 | Readiness recompute failed (see logs) |
Bulk Import (PR-3)¶
Source: docs/specs/procedure-onboarding-admin-ui.md §9.2 / PR-3.
All codes returned by /api/v1/admin/procedures/bulk-import/*.
| Code | HTTP | Meaning |
|---|---|---|
BULK_IMPORT_PARSE_ERROR_422 |
422 | YAML or CSV could not be parsed (syntax error) |
PROCEDURE_BULK_IMPORT_VALIDATION_422 |
422 | Payload has per-row validation errors; see details[] for row-level info |
BULK_IMPORT_ROW_FAILED_500 |
500 | A row failed during DB apply; entire batch rolled back |
Capability Vocabulary (admin)¶
Source: docs/specs/procedure-onboarding-admin-ui.md §9.2 PR-4.
All codes returned by /api/v1/admin/capability-vocabulary/*.
| Code | HTTP | Meaning | Cause | Resolution |
|---|---|---|---|---|
PROCEDURE_ADMIN_DISABLED_001 |
503 | Feature flag procedure_admin_ui_enabled is off |
Flag not enabled in Flagsmith | Enable the flag for the environment |
CAP_VOCAB_NOT_FOUND_404 |
404 | capability_code not in the vocabulary catalog |
Stale code or typo | Verify code via GET /admin/capability-vocabulary list |
CAP_VOCAB_DUPLICATE_409 |
409 | Capability code already exists in the catalog | Double-create or race | The code is already present; use PATCH to update |
CAP_VOCAB_REFERENCED_409 |
409 | Cannot delete — code is still referenced in provider or procedure assignments | Assignments exist for this code | Remove all provider_capabilities + procedure_capability_requirements rows first |
CAP_VOCAB_INVALID_CODE_422 |
422 | capability_code fails pattern ^[A-Z0-9_\-]{2,64}$ |
Lowercase, spaces, or special chars | Use uppercase alphanumeric + underscore/hyphen only |
Recovery Milestones (admin)¶
Source: docs/specs/admin-ui-gap-audit-960-data.md § Chain D / PR-D1.
All codes returned by /api/v1/admin/procedures/{code}/recovery-milestones.
| Code | HTTP | Meaning |
|---|---|---|
PROCEDURE_ADMIN_DISABLED_001 |
503 | Feature flag procedure_admin_ui_enabled is off |
RECOVERY_NEEDS_NOT_FOUND_404 |
404 | procedure_code has no row in config/recovery_needs/seed.yaml |
Recompute Jobs (PR-6)¶
Source: docs/specs/procedure-onboarding-admin-ui.md §9.2 / PR-6.
All codes returned by /api/v1/admin/procedures/recompute-readiness/*.
| Code | HTTP | Meaning |
|---|---|---|
RECOMPUTE_JOB_NOT_FOUND_404 |
404 | No recompute job with the given job_id |
Transport Vendor Admin¶
Source: docs/specs/transport-vendor-admin-feature.md §5.5.
All codes returned by POST/PATCH/DELETE /api/v1/transport/vendors/*.
| Code | HTTP | Meaning |
|---|---|---|
TRANSPORT_VENDOR_SLUG_EXISTS |
409 | Create request slug collides with an existing providers.slug in the tenant. |
TRANSPORT_VENDOR_NOT_FOUND |
404 | Update / delete target vendor not in tenant or transport_provider_profiles row missing. |
TRANSPORT_VENDOR_ALREADY_INACTIVE |
409 | Soft-delete attempted on a vendor with providers.is_active = false. |
TRANSPORT_VENDOR_ALREADY_ACTIVE |
409 | Reactivate attempted on a vendor with providers.is_active = true. |
TRANSPORT_VENDOR_VEHICLE_CLASS_MISMATCH |
422 | vendor_class=ground_accessible requires wheelchair_accessible in vehicle_types; vendor_class=medical requires medical_van or ambulance_basic. |
TRANSPORT_VENDOR_CURRENCY_COUNTRY_MISMATCH |
422 | pricing_currency doesn't align with service_country per the ISO-4217↔ISO-3 alignment table (spec §7 rule 6). |
TRANSPORT_VENDOR_PRICING_INCOMPLETE |
422 | pricing_model=per_km without per_km_minor_units, or pricing_model=flat_per_trip without base_fare_minor_units, or pricing_model=hybrid without both. |
TRANSPORT_VENDOR_AIR_RESERVED |
422 | vendor_class=air rejected — reserved for Phase 7b. |