Errores

Toda respuesta que no sea 2xx es un documento RFC 9457 application/problem+json. Programa contra code, que es estable y append-only; nunca contra detail, que es una frase para un humano y puede cambiar de redacción.

Forma del error

Plano, sin sobre anidado. Los cinco miembros del RFC más las extensiones de Belfort:

{
  "type": "https://belfort.ai/docs/errors#CURP_CONFLICT",
  "title": "CURP already in use",
  "status": 409,
  "detail": "Another active person in this tenant already has this CURP.",
  "instance": "/v1/persons",
  "code": "CURP_CONFLICT",
  "requestId": "9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d",
  "conflictingPersonId": "per_1640d5665d1c41fda8671087f4ebc960"
}

El type es un enlace a esta misma página, anclado al código. Si recibes un error que no entiendes, ábrelo: llega a su fila.

Extensiones por código

Algunos códigos añaden campos propios al documento, y son la parte accionable: CURP_CONFLICT trae conflictingPersonId —la persona que ya tenía esa CURP—, INSUFFICIENT_SCOPE trae requiredScope, y los 429 viajan con Retry-After en las cabeceras.

Las extensiones se camelizan en el nombre; el valor va tal cual. Un scope como persons:read_pii nunca se cameliza: es un dato, no una clave.

Errores por campo

La validación (422 VALIDATION_ERROR) puebla errors[] con un objeto por campo. Lleva field y pointer a propósito: field es la ruta que reconoces de tu propio payload, y pointer es el JSON Pointer de RFC 6901, que es lo que consume una herramienta genérica. Dar solo uno obliga a la mitad de los integradores a parsear el otro formato.

"errors": [
  { "field": "curp", "pointer": "/curp",
    "code": "invalid_format", "detail": "CURP must be 18 characters" }
]

Correlación con Belfort-Request-Id

Cada respuesta trae la cabecera Belfort-Request-Id, y el mismo valor aparece como requestId dentro del cuerpo del error. Es la llave para rastrear la petición: cítalo al reportar un problema y nos ahorras la mitad de la conversación.

Si mandas tu propio X-Request-Id, se acepta y se registra — pero el canónico sigue siendo el nuestro, para que dos peticiones distintas no puedan compartir identificador.

Catálogo de códigos

151 códigos, generados del mismo enum que los emite (app/errors/codes.py en el backend). El catálogo es append-only: un código publicado no se renombra ni se borra, porque el if de tu integración dejaría de entrar sin dar ningún error. 42 llevan una explicación de por qué existen y contra qué contrastan.

Una fila marcada como reservada existe en el catálogo para una superficie futura, pero hoy ningún endpoint la emite.

Verificación

CódigoStatusQué significa
ADMISSION_FULL 503 Service at capacity
ALLOWED_ORIGINS_REQUIRED 422 El canal embebido exige allowedOrigins configurado. Hermano de FLOW_REQUIRED.
CAPTURE_CREDENTIAL_INVALID 403 Credencial de un solo uso del canal de captura (nonce o challenge de liveness) inválida, caducada o ya gastada. UN código para los dos controles: mismo remedio (pide otra y reintenta) y mismo mensaje constante anti-fingerprinting. Distinto de CAPTURE_NOT_ALLOWED, que es "este paso no pertenece a tu flow" y se arregla en el flow.
CAPTURE_NOT_ALLOWED 409 Capture not allowed
INVALID_STATE_TRANSITION 409 Invalid state transition
ORIGIN_NOT_ALLOWED 403 Origin not allowed
SESSION_ALREADY_USED 409 Session credential already used
SESSION_CANCELLED 409 Session cancelled
SESSION_COMPLETED 409 Session already completed
SESSION_EXPIRED 401 Session expired
SESSION_INVALID 401 Invalid session credential
SESSION_NOT_FOUND 404 Verification session not found
SESSION_NOT_SUBMITTABLE 409 Session is not in a submittable state

Autenticación y credenciales

CódigoStatusQué significa
API_KEY_INVALID 401 Invalid API key
APPLICATION_ARCHIVED 403 Application archived
INSUFFICIENT_SCOPE 403 Insufficient scope
NO_ACTIVE_ORGANIZATION 403 No active organization
PUBLISHABLE_KEY_NOT_ALLOWED reservado 403 Publishable key not allowed
PUBLISHABLE_KEY_REQUIRED 403 El endpoint EXIGE una publishable (pk_). Completa la familia de tipo-de-key: es la dirección contraria de PUBLISHABLE_KEY_NOT_ALLOWED, y emitir aquel diría lo opuesto.
SERVICE_KEY_REQUIRED 401 Service key required
TENANT_NOT_FOUND 404 Autenticado (JWT o service key válidos) pero el tenant como el que actuaría no existe. Es 404, no 401: la credencial se verificó entera. NO_ACTIVE_ORGANIZATION es lo contrario (sesión SIN organización); aquí hay una, solo que desconocida.
TEST_KEY_NOT_ALLOWED 403 Test-mode key not allowed

Applications / flows / developers

CódigoStatusQué significa
API_KEY_NOT_FOUND 404 API key not found
APPLICATION_NAME_TAKEN 409 Application name already taken
APPLICATION_NOT_FOUND 404 Application not found
CANNOT_ARCHIVE_DEFAULT 409 The default application cannot be archived
DEFAULT_FLOW_NOT_IN_ALLOWED 422 Default flow is not in the allowed set
FLOW_KEY_TAKEN 409 La key del flow ya está tomada en este tenant. Hermano de APPLICATION_NAME_TAKEN.
FLOW_NOT_ALLOWED_FOR_APPLICATION 403 Flow not allowed for this application
FLOW_NOT_ALLOWED_FOR_KEY 403 El flow no está en el allowlist de ESTA API key. Independiente de FLOW_NOT_ALLOWED_FOR_APPLICATION: se arreglan en sitios distintos (ApiKey.allowed_flows vs Application.allowed_flows) y las dos guardas deben pasar.
FLOW_NOT_DELETABLE 409 El flow no se puede BORRAR (publicado/archivado, o borrador que ya minteó una sesión). Dirección contraria de FLOW_NOT_PUBLISHABLE; la salida del cliente es archivar.
FLOW_NOT_FOUND 404 Flow not found
FLOW_NOT_PUBLISHABLE 409 Flow cannot be published
FLOW_REQUIRED 422 A flow is required
FLOW_VALIDATION_FAILED 422 El grafo del flow (o su versión draft) no pasa la validación de publish (steps vacíos, checks en conflicto, referencia a un step_key que no existe, etc.). Distinto de FLOW_NOT_PUBLISHABLE (que es sobre el ESTADO del flow, p. ej. archivado) — aquí el flow podría publicarse si su contenido fuera válido. Lleva la lista completa de issues en la extensión issues.
NOT_ALLOWED_BY_APPLICATION 422 Al escribir una publishable key: el valor excede lo que permite su Application. Es la invariante "una pk nunca es MÁS permisiva que la Application que la agrupa".
ORIGIN_CONFLICT 409 Origin conflict
ORIGIN_INVALID 422 Un allowedOrigins (Application/pk_) no es HTTPS-con-host-concreto ni localhost/loopback (origins.invalid_origins). Hermano de WEBHOOK_URL_INVALID: mismo shape de problema ("esta URL/origen no tiene la forma que exige el contrato"), dominio distinto.
SCOPE_NOT_ALLOWED_FOR_KEY_TYPE 422 Scope prohibido para ese tipo de key (una pk_ es flow-only por diseño). Distinto de UNKNOWN_SCOPE (no existe) y de INSUFFICIENT_SCOPE (denegación en runtime).
UNKNOWN_EVENT_TYPE 422 Unknown event type
UNKNOWN_SCOPE 422 Unknown scope
WEBHOOK_DELIVERY_NOT_FOUND 404 La ENTREGA de webhook no existe. Recurso propio, distinto del endpoint: reusar WEBHOOK_NOT_FOUND agruparía bajo un title que nombra el recurso equivocado.
WEBHOOK_ENDPOINT_INACTIVE 409 Reenvío imposible: la entrega existe pero su endpoint está inactivo o fue borrado.
WEBHOOK_NOT_FOUND 404 Webhook endpoint not found
WEBHOOK_URL_INVALID reservado 422 Invalid webhook URL

Legal / ARCO

CódigoStatusQué significa
ARCO_INVALID_TRANSITION 409 Invalid ARCO state transition
ARCO_REQUEST_NOT_FOUND 404 ARCO request not found
ARCO_SUBJECT_REQUIRED 422 ARCO request requires a subject

Genéricos por status

CódigoStatusQué significa
AUTHENTICATION_REQUIRED 401 Authentication required
BAD_REQUEST 400 Bad request
CONFLICT 409 Conflict
GONE 410 Gone
INTERNAL_ERROR 500 Internal server error
METHOD_NOT_ALLOWED 405 Method not allowed
NOT_IMPLEMENTED reservado 501 Not implemented
PAYLOAD_TOO_LARGE 413 Payload too large
PERMISSION_DENIED 403 Permission denied
RATE_LIMITED 429 Too many requests
RESOURCE_NOT_FOUND 404 Resource not found
SERVICE_UNAVAILABLE 503 Service unavailable
UNSUPPORTED_MEDIA_TYPE 415 Unsupported media type
VALIDATION_ERROR 422 Validation error

Persons vault ops

CódigoStatusQué significa
BATCH_TOO_LARGE 413 Batch too large
CANNOT_MERGE_INTO_SELF 400 Cannot merge a person into itself
CLIENT_TOKEN_INVALID 401 Invalid or expired client token
CLIENT_TOKEN_NOT_FOUND 404 Client token not found
CONSOLIDATION_NOT_FOUND 404 Consolidation not found
DELETION_NOT_COMPLETE 409 Certificado de borrado pedido antes de que la purga real haya corrido.
ENVIRONMENT_MISMATCH 400 Environment mismatch
EXPORT_DUMP_EXPIRED 409 Export dump has expired
EXPORT_DUMP_NOT_READY 409 Export dump is not ready yet
EXPORT_JOB_NOT_FOUND 404 Export job not found
FIELD_NOT_GRANTED 403 Field not granted
FIELD_NOT_IN_TOKEN_SCOPE 403 Field not in client-token scope
FORMAT_INVALID 422 Invalid export format
GRANT_NOT_FOUND 404 Grant not found
GRANT_PURPOSE_NOT_ALLOWED 403 Grant does not allow this purpose
IMPORT_BATCH_TOO_LARGE 413 Import batch too large
IMPORT_BODY_INVALID 400 Invalid import body
IMPORT_JOB_NOT_FOUND 404 Import job not found
INTEGRITY_FIELD_UNSUPPORTED 400 Unsupported integrity field
INTEGRITY_NO_FIELDS 400 No fields requested
INTEGRITY_SIGNING_KEY_INVALID 400 Invalid integrity signing key
METADATA_INVALID 400 El vocabulario de code que consume el SDK de /v1/persons (antes un segundo dialecto {code, reason}, ver app/api/conventions/errors.py) vive aquí desde W-contrato: mismo significado por condición, ahora expresado como problem+json. Códigos con equivalente preexistente en el catálogo (p.ej. curp_conflict→CURP_CONFLICT) NO se repiten aquí.
MODE_INVALID 422 mode de DELETE /v1/persons/{id} (block|erase) no reconocido.
PERSON_ALREADY_MERGED 409 Person has already been merged
PERSON_FORGET_PENDING 410 Solicitud de olvido en curso (aún no purgada del todo) — mismo status que PERSON_PURGED por la misma razón: no hay PII que seguir revelando mientras el olvido está en camino.
PERSON_PURGED 410 Person ya purgada (derecho al olvido consumado) — 410, no 404: existió, y ya no hay nada que revelar nunca más (a diferencia de PERSON_NOT_FOUND, que no afirma que haya existido).
PURPOSE_UNKNOWN 400 Unknown processing purpose
REASON_REQUIRED 422 Falta reason al expandir PII en claro por el canal de API key (persons:read_pii).
REAUTH_ENGINE_UNAVAILABLE 503 Re-auth biométrica 1:1: el motor de comparación (idv-api) no respondió/falló.
REAUTH_MEDIA_UNAVAILABLE 409 Re-auth biométrica 1:1 (B-G9): no hay selfie retenida utilizable de una verificación previa.
REPORT_TYPE_INVALID 422 Invalid report type
SURVIVOR_NOT_ACTIVE 409 Survivor is not active

Facturación / créditos

CódigoStatusQué significa
BILLING_NOT_CONFIGURED 503 Billing is not configured
INSUFFICIENT_CREDITS 402 Insufficient credits
NOT_A_REVIEWER 422 Reasignar un caso a alguien que no es revisor de ESE tenant. 422 y no 403: no es que al llamante le falte permiso, es que el valor que mandó no es válido para este tenant.
RESERVATION_NOT_FOUND 404 La reserva de créditos (CreditReservation) referenciada no existe o no pertenece al tenant — anti-IDOR vía tenant_id en el WHERE, mismo criterio que el resto del catálogo.
RESERVATION_NOT_PENDING 409 settle/release exigen una reserva en estado PENDING; ya está SETTLED (consumida) o RELEASED (liberada) y la operación pedida no aplica dos veces.
STRIPE_CUSTOMER_MISSING 409 El tenant existe pero aún no tiene cliente en Stripe (nunca pasó por checkout).
STRIPE_SIGNATURE_INVALID 401 Firma de un webhook ENTRANTE de Stripe que no valida contra STRIPE_WEBHOOK_SECRET. Nadie más que Stripe llama a este endpoint, pero el código sigue siendo la CONDICIÓN, no el síntoma genérico de un 401.
TENANT_PROFILE_NOT_FOUND 404 El TenantProfile (perfil de facturación) no existe todavía para un tenant autenticado. Invariante de provisioning, no algo que un integrador dispare normalmente — distinto de TENANT_NOT_FOUND (que es sobre el TENANT en sí, no su perfil de billing).
TRIAL_LIMIT_REACHED 429 Cupo de una key de trial agotado. Es un 429 como RATE_LIMITED pero NO se arregla esperando, así que sale sin Retry-After — y por eso necesita código propio.

Revisión manual

CódigoStatusQué significa
CASE_ALREADY_CLAIMED 409 Case already claimed
CASE_CLAIM_REQUIRED 409 La acción exige sostener el lock del caso y quien la pide no lo tiene. Es el único de la familia con remedio accionable (POST /review/cases/{id}/claim); CASE_ALREADY_CLAIMED mentiría cuando el caso está libre.
CASE_CLOSED 409 Case is closed
CASE_HAS_NO_SESSION 409 resolve() exige un session_id (aprobar/declinar/pedir-info/escalar actúan sobre una VerificationSession); un caso manual sin sesión (p. ej. sin sesión asociable) no tiene remedio vía esta acción. Caso extremo, no una violación de rol/lock — código propio.
CASE_NOT_FOUND 404 Case not found
SECOND_APPROVAL_REQUIRED reservado 409 A second approval is required

Applicant / flows

CódigoStatusQué significa
CHALLENGE_ID_REQUIRED 400 Falta challenge_id en un POST .../captures de kind=liveness_video en modo activo.
CHECK_NOT_ENABLED_FOR_FLOW 400 El endpoint no aplica a la config de ESTE flow (liveness activo, efirma_sat, aml_screening, proof_of_address...). Un solo código para las cuatro superficies: mismo remedio (revisa la config del flow) y mismo shape de respuesta; la extensión check dice cuál.
INVALID_CAPTURE_PLATFORM 422 Invalid capture platform
TEST_MODE_CONFLICTS_WITH_KEY_ENVIRONMENT 400 Un test_mode=true del body choca con una API key sk_live_... (el environment de la key SIEMPRE gana). Nombre explícito para que el mensaje se explique solo en el type del error.

KYB / AML

CódigoStatusQué significa
COMPANY_NOT_FOUND 404 Company not found
DISPUTE_NOT_FOUND 404 Dispute not found
PORTFOLIO_NOT_FOUND 404 Portfolio not found
RFC_CONFLICT 409 RFC already claimed
SCREENING_NOT_FOUND reservado 404 Screening not found
UBO_NODE_NOT_FOUND 404 UBO node not found

Personas / vault de PII

CódigoStatusQué significa
CURP_CONFLICT 409 CURP already in use
PERSON_BLOCKED reservado 409 Person is blocked
PERSON_NOT_FOUND 404 Person not found
PII_ACCESS_REASON_REQUIRED 422 record_access exige reason para los canales persons_api/client_token (BEL-420) — backstop fail-closed; en la práctica el reason siempre viaja desde el ClientToken o el endpoint de Persons antes de llegar aquí.
PII_DECRYPT_CAP_REACHED 429 PII decryption cap reached
UNKNOWN_PII_FIELD 422 Unknown PII field
VERIFIED_FIELD_CONFLICT reservado 409 Government-verified field cannot be overwritten

e.firma SAT — prueba de posesión

CódigoStatusQué significa
EFIRMA_CERT_NOT_READY 400 issue/verify_possession piden un challenge de posesión antes de que el check cert-only (B-G3) haya resuelto valid para la sesión — firmar un challenge sobre un cert todavía no confiable no prueba nada útil de identidad.
EFIRMA_CHALLENGE_ALREADY_USED 409 El challenge ya se consumió (single-use) — replay o carrera perdida contra otro intento.
EFIRMA_CHALLENGE_EXPIRED 410 El challenge venció (TTL ~2 min) sin usarse.
EFIRMA_CHALLENGE_NOT_FOUND 404 El challengeId no existe o pertenece a otra sesión. Sin mensaje constante anti-fingerprinting (a diferencia de CAPTURE_CREDENTIAL_INVALID): el id es un UUID server-random que solo viaja al dueño de la sesión, no hay nada que adivinar.

Idempotencia

CódigoStatusQué significa
IDEMPOTENCY_KEY_IN_PROGRESS 409 A request with this Idempotency-Key is in progress
IDEMPOTENCY_KEY_INVALID 400 Invalid Idempotency-Key
IDEMPOTENCY_KEY_REQUIRED 400 Idempotency-Key required
IDEMPOTENCY_KEY_REUSED 409 Idempotency-Key reused with different parameters

Identificadores y forma del request

CódigoStatusQué significa
INVALID_CURSOR 400 Invalid cursor
INVALID_ID 400 Invalid identifier
LIMIT_OUT_OF_RANGE 422 Limit out of range
PII_IN_PLAINTEXT_FIELD reservado 422 Personal data in a plaintext field

Media

CódigoStatusQué significa
MEDIA_CONTENT_INVALID 422 Los bytes subidos no sirven (vacío, o el contenido no casa con el mime declarado). El remedio es RE-CAPTURAR, no corregir el payload — por eso no es VALIDATION_ERROR.
MEDIA_NOT_FOUND 404 Media asset not found
UNKNOWN_MEDIA_KIND 422 El kind nombrado no es una clase de media que Belfort conozca. Misma familia que UNKNOWN_SCOPE/UNKNOWN_EVENT_TYPE: "nombraste un miembro de enum que no existe".

Notificaciones

CódigoStatusQué significa
NOTIFICATION_NOT_ACTIONABLE 422 El verbo existe pero esta notificación no apunta a nada accionable (sin subject, subject de tipo no accionable, o sin proveedor válido). Distinto de UNKNOWN_NOTIFICATION_ACTION, que es sobre el VERBO, y de NOTIFICATION_NOT_FOUND, que es sobre la existencia.
NOTIFICATION_NOT_FOUND 404 Notification not found
UNKNOWN_NOTIFICATION_ACTION 422 Unknown notification action
UNKNOWN_NOTIFICATION_CATEGORY 422 Unknown notification category

Versionado

CódigoStatusQué significa
UNSUPPORTED_API_VERSION 400 Unsupported API version

Red / SSRF

CódigoStatusQué significa
URL_NOT_PUBLIC 422 URL is not publicly reachable