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.
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ódigo | Status | Qué 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 |
CONSENT_REQUIRED |
409 |
Falta el consentimiento LFPDPPP para enviar. NO es un estado de la sesión (puede estar perfectamente in_progress): mandar al integrador a mirar status lo haría depurar el ciclo de vida en vez de llamar a POST /consent. |
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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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 |
CONSENT_NOT_FOUND |
404 |
Consent 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_LEGAL_HOLD |
409 |
Person is under legal hold |
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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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ódigo | Status | Qué 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_LINK_INVALID |
403 |
Invalid or expired media link |
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ódigo | Status | Qué 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ódigo | Status | Qué significa |
UNSUPPORTED_API_VERSION |
400 |
Unsupported API version |
Red / SSRF
| Código | Status | Qué significa |
URL_NOT_PUBLIC |
422 |
URL is not publicly reachable |