Errores genéricos
Códigos a nivel de status, compartidos por las tres APIs.
Los code de 404 y 409 son específicos de dominio por defecto: una regla de negocio (409) siempre lleva un code que nombra el invariante violado (domain_already_exists, sender_already_exists, password_incorrect, ...), y un recurso inexistente (404) lleva <recurso>_not_found. Los códigos genéricos not_found y conflict que se listan abajo son el contrato del framework (404 de ruta inexistente) o el fallback de la familia, no lo que normalmente observa un integrador.
Regla de compatibilidad: agregar o promover un code específico es compatible; degradarlo a uno más genérico es ruptura.
Códigos por status
bad-request
- Cuándo
- Petición sintácticamente válida pero rechazada antes de la validación de esquema (p. ej. parámetro de query malformado).
- detail
- Descriptivo.
unauthorized
- Cuándo
- Falta el
Authorization, el token es inválido o expiró, o la API key es desconocida/revocada. - detail
- Genérico (
"Authentication required.") — no distingue token inválido de expirado. En la superficie de API key deinstance-api(/v1/*) este 401 cubre además el caso de unX-Instance-Slugque no corresponde a ninguna instancia. Es deliberado: la respuesta es idéntica —status, cuerpo y cabeceras— a la de una key inválida, para que un llamador sin credencial no pueda averiguar qué slugs existen. Si estás integrando y recibís un 401 inesperado, revisá las dos cosas: la key y el slug. Todo 401 incluye la cabeceraWWW-Authenticate: Bearer, sinrealmnierror(RFC 6750 §3.1). La ausencia de parámetros es parte del contrato: el motivo real vive solo en el log, correlacionable con elX-Request-Idde la respuesta.
forbidden
- Cuándo
- El llamador está autenticado pero no autorizado: scope insuficiente, rol incorrecto, o la instancia de destino está deshabilitada (una API key nunca opera una instancia deshabilitada, #537).
- detail
- Genérico (
"Access denied."); el motivo real solo va al log estructurado. - Ver también
- La denegación por dirección de origen tiene su propio
code— ver ip-not-allowed. En las rutas privadas deinstance-api(autenticadas con JWT) este 403 cubre también elX-Instance-Slugque no existe, con la misma respuesta que un slug ajeno: un llamador sin acceso no puede distinguir "no existe" de "no es tuyo". Un llamador que sí está autorizado sobre ese slug —o un super admin, un llamador S2S o el staff de backoffice— recibe el404correspondiente.
ip-not-allowed
- Cuándo
- La dirección de origen de la petición no está permitida. Lo emiten los dos controles de IP: la whitelist de la instancia (
instance.whitelist, que aplica a todos los endpoints privados y de integración) y la de la credencial (credential.ip_whitelist, en la superficie de API key). - detail
- El mismo genérico que cualquier 403 (
"Access denied."): elcodees lo único que distingue. Deliberadamente no se informa cuál de los dos controles rechazó la petición, ni las direcciones o rangos configurados. Solo lo recibe un llamador ya autenticado y autorizado sobre la instancia. Una petición sin credencial válida desde una dirección no permitida obtiene el401/403genérico que corresponda a su falta de credencial, nunca este código — de lo contrario revelaría que el tenant existe y tiene whitelist configurada. - Qué hacer
- Revisar desde qué dirección sale el tráfico y compararla con la whitelist configurada para la instancia y para la API key en uso. Si el tráfico sale por un NAT o un proxy, la dirección que evalúa el control es la pública de salida, no la interna del host.
not-found
- Cuándo
- El recurso no existe.
- detail
- Descriptivo:
"{recurso} '{id}' not found". - Código
- El
codees específico del recurso: un 404 de dominio emitedomain_not_found, uno de remitentesender_not_found, etc. (patrón<recurso>_not_found, con sutype.../errors/<recurso>-not-found). Elcodegenériconot_foundsolo lo emite el framework ante una ruta inexistente (un 404 que no corresponde a ningún recurso de negocio).
mail-outside-retention
- Cuándo
- Variante del 404 de mail en las consultas por
mail_id(GET /v1/mails/{mail_id}y.../logs): el identificador lleva la fecha de recepción, y esa fecha es anterior a la ventana de retención en línea (3 meses), así que el mail ya no está disponible aunque haya existido. - Distinción
- La distinción con
mail_not_foundes accionable:mail_not_foundsignifica "revise el id";mail_outside_retention, "el id es demasiado antiguo para estar en línea".
method-not-allowed
- Cuándo
- El path existe pero no acepta el verbo HTTP usado.
conflict
- Cuándo
- Violación de unicidad o regla de negocio.
- Código
- El
codees siempre específico de dominio y nombra el invariante violado — el genéricoconflictno se emite. Un integrador haceswitchsobre estecode. La fuente autoritativa de la lista es el argumentocode=de cadaraise ConflictError(..., code=...)en los servicios (incluido el sync de billing S2S, cuyos códigos por ítem viajan dentro del body 200 del batch). - Ver también
- La lista de conflictos por dominio está en Conflictos de dominio (409).
Códigos de la API pública (integradores)
Códigos exclusivos de la API privada (panel)
⚠️ Esta tabla es de referencia interna, no parte del contrato de integración. Estos códigos solo los emiten rutas
/v1/private/*(JWT, consumidas por el panel/frontend) — un llamador con API key nunca los recibe. No llevan el mismo compromiso de estabilidad que la tabla anterior: pueden cambiar o desaparecer sin el mismo aviso de ruptura que un código público.
code | Disparador |
|---|---|
domain_is_sandbox | La operación no aplica al dominio del entorno de pruebas, que administra la plataforma: no se puede eliminar ni pedir el envío de sus registros DNS. |
template_content_required | Se intentó publicar una plantilla sin contenido HTML (#774): no habría nada que entregar. |
confirmation_requires_email | Se intentó activar un formulario con doble confirmación sin un campo Email marcado como obligatorio — no habría destino donde enviar el correo de confirmación (#863). |
consent_event_not_pending | Se intentó reenviar la confirmación de un evento de consentimiento que ya no está pendiente (#863). |
consent_event_no_submission | Se intentó reenviar la confirmación de un evento de consentimiento que no proviene de un envío de formulario (#863). |
form_submission_not_pending | El envío de formulario detrás del evento de consentimiento ya no está pendiente de confirmación (#863). |
consent_term_missing_channels_or_purposes | Salvaguarda que hoy no debería poder dispararse: canal y finalidad son obligatorios al crear un término (#906), así que ningún término puede llegar a la activación sin alguno de los dos. |
request-too-large
- Cuándo
- La petición supera alguna de las cotas de tamaño. El cuerpo
problem+jsonincluyemax_bytescuando el rechazo viene de la cota del cuerpo. Causas: Las cotas concretas de/mails/sendestán en su ficha. - El cuerpo de la petición supera el máximo permitido para la ruta.
- El cuerpo llega comprimido y supera el máximo al descomprimirlo.
- Un adjunto individual de
multipart/form-datasupera su límite. - El mensaje ya armado supera el tamaño máximo aceptado para envío.
- El correo supera la capacidad de ráfaga del límite de ancho de banda de la instancia.
unsupported-media-type
- Cuándo
- El
Content-Typeenviado no es el esperado (típicamente, no es JSON).
validation-error
- Cuándo
- Falla de validación de esquema.
- Cuerpo
- El cuerpo incluye
errors[]— ver Errores por campo (422).
rate-limit-exceeded
- Cuándo
- Se superó el límite de ancho de banda de la instancia.
- Cabeceras
- La respuesta incluye el header
Retry-After(segundos a esperar antes de reintentar).
service-unavailable
internal-error
- Cuándo
- Error no manejado.
- detail
- Siempre genérico (
"Internal server error."); el error real (con stack) va al log a nivelerror.
Conflictos de dominio (409)
Dominios y remitentes
domain_already_existsEl dominio ya existe en la instancia.
domain_not_verifiedEl dominio aún no tiene las verificaciones DNS aprobadas.
domain_sender_not_verifiedEl dominio del remitente no está verificado.
sender_already_existsYa existe un remitente con ese email.
Plantillas
template_not_publicLa plantilla referida no es pública.
Credenciales y sandbox
api_key_scope_requiredaccess_type=scoped sin ningún permiso asignado.
sandbox_relay_restrictedUn remitente sandbox intentó enviar a destinatarios fuera de sandbox.
Usuarios
user_already_memberEl email agregado ya es miembro de esta instancia (add-member create-or-link, #628). Es el único conflicto visible de ese flujo: la existencia global de un email nunca se revela.
Campos y datasets
field_key_already_existsYa existe un campo activo con ese field_key (#732).
field_key_reservedEl field_key perteneció a un campo eliminado; queda reservado para siempre y no puede reutilizarse (#732).
field_is_systemSe intentó eliminar un campo de sistema (#732).
dataset_import_field_id_requiredUna columna del import está mapeada (action=map) sin field_id (#732).
dataset_import_field_not_foundUna columna del import mapea a un field_id inexistente (#732).
Consentimiento
consent_term_inactiveSe intentó registrar un consentimiento contra un término desactivado (#732, API pública).
consent_channel_not_coveredUn ítem de consentimiento de un formulario declara un canal (mail/sms/wsp) sin ningún campo del field_key correspondiente presente en el formulario (#862).
consent_term_no_current_versionEl término no tiene ninguna versión vigente (is_current) contra la cual registrar el consentimiento (#732, API pública).
consent_contact_type_channel_mismatchEl tipo de identificador no es válido para el canal de la regla — p. ej. una dirección de correo en un canal push, o un teléfono en mail (#824, API pública).
consent_duplicate_ruleUna misma llamada trae dos reglas para el mismo par canal + finalidad (#824, API pública).
consent_contact_pairs_mismatchAl resolver el consentimiento vigente, las listas de identificadores y de tipos vienen con distinta cantidad de elementos: se emparejan por posición (#824, API pública).
Los más comunes en la API pública (instance-api).
Las APIs de control (saascore-api, services-api) definen códigos 409 adicionales para su propio dominio (gestión de usuarios, instancias, hosts de BD, accesos de staff). detail descriptivo con el mensaje del caso (en inglés).
Errores por campo (422)
Un 422 agrega el array errors[], un ítem por campo que falló. Cada ítem:
| Campo | Significado |
|---|---|
pointer | JSON Pointer (RFC 6901) al campo, sin el segmento de origen. ("body","recipients",0,"email") → /recipients/0/email. Un error de raíz del body es "". |
code | El type de error de Pydantic (missing, string_too_short, ...). La clave estable por campo para localización ICU — pásala a tu localizador junto con params. |
message | Mensaje legible en inglés (desde Pydantic). Fallback para integradores; el frontend usa code. |
params | Contexto del error (ctx de Pydantic); solo presente cuando no está vacío. Los escalares (ej. { "min_length": 3 }) se pasan directamente a ICU para interpolar el mensaje. Cualquier excepción embebida se serializa a string. |
Valores frecuentes de errors[].code (lista completa: tipos de error de Pydantic v2):
code | Significado |
|---|---|
missing | Campo requerido ausente. |
string_too_short / string_too_long | Fuera del rango de longitud. |
string_pattern_mismatch | No cumple el patrón (regex) declarado. |
value_error | Validador custom falló (incluye variantes con sufijo, p. ej. value_error.<custom>). |
int_parsing / bool_parsing / float_parsing | Coerción de tipo falló. |
enum | Valor fuera del conjunto permitido. |
Ejemplo:
{
"type": "https://docs.fidelizador.com/errors/validation-error",
"title": "Unprocessable Content",
"status": 422,
"detail": "The request failed validation.",
"code": "validation_error",
"trace_id": "a7f702cba572544de30d845b9560f8d7",
"errors": [
{
"pointer": "/field_context",
"code": "enum",
"message": "Input should be 'company', 'system' or 'billing'",
"params": { "expected": "'company', 'system' or 'billing'" }
}
]
}