Saltar al contenido principal

Errores genéricos

Códigos a nivel de status, compartidos por las tres APIs.

info

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.

aviso

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 de instance-api (/v1/*) este 401 cubre además el caso de un X-Instance-Slug que 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 cabecera WWW-Authenticate: Bearer, sin realm ni error (RFC 6750 §3.1). La ausencia de parámetros es parte del contrato: el motivo real vive solo en el log, correlacionable con el X-Request-Id de 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 de instance-api (autenticadas con JWT) este 403 cubre también el X-Instance-Slug que 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 está autorizado sobre ese slug —o un super admin, un llamador S2S o el staff de backoffice— recibe el 404 correspondiente.

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."): el code es 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 el 401/403 gené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 code es específico del recurso: un 404 de dominio emite domain_not_found, uno de remitente sender_not_found, etc. (patrón <recurso>_not_found, con su type .../errors/<recurso>-not-found). El code genérico not_found solo 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_found es accionable: mail_not_found significa "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 code es siempre específico de dominio y nombra el invariante violado — el genérico conflict no se emite. Un integrador hace switch sobre este code. La fuente autoritativa de la lista es el argumento code= de cada raise ConflictError(..., code=...) en los servicios (incluido el sync de billing S2S, cuyos códigos por ítem viajan dentro del body 200 del batch).

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.

codeDisparador
domain_is_sandboxLa 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_requiredSe intentó publicar una plantilla sin contenido HTML (#774): no habría nada que entregar.
confirmation_requires_emailSe 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_pendingSe intentó reenviar la confirmación de un evento de consentimiento que ya no está pendiente (#863).
consent_event_no_submissionSe intentó reenviar la confirmación de un evento de consentimiento que no proviene de un envío de formulario (#863).
form_submission_not_pendingEl envío de formulario detrás del evento de consentimiento ya no está pendiente de confirmación (#863).
consent_term_missing_channels_or_purposesSalvaguarda 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.
Ver también
La lista de conflictos por dominio está en Conflictos de dominio (409).

request-too-large

Cuándo
La petición supera alguna de las cotas de tamaño. El cuerpo problem+json incluye max_bytes cuando el rechazo viene de la cota del cuerpo. Causas: Las cotas concretas de /mails/send está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-data supera 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-Type enviado 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

Cuándo
Una dependencia de la que depende la operación no respondió. En el caso de la verificación de dominio, el 503 no es un veredicto sobre el dominio. Los estados por protocolo (mx_status, spf_status, dkim_status, dmarc_status) quedan sin modificar, así que un dominio ya verificado sigue sirviendo para enviar. Distinto de un 200 con protocolos en failed, que sí afirma que los registros publicados no cumplen.
Código
El code es el mismo para todas porque cada una responde en su propio endpoint; el detail nombra la dependencia:
detailEndpointQué pasó
Mail ingest temporarily unavailable.POST /v1/mails/sendLa ingesta no pudo publicar a Pulsar tras agotar los reintentos.
DNS verification temporarily unavailable.POST /v1/private/domains/{id}/validateLa consulta DNS quedó sin respuesta: ni el resolver ni su fallback contestaron.
Qué hacer
Transitorio: reintentar.

internal-error

Cuándo
Error no manejado.
detail
Siempre genérico ("Internal server error."); el error real (con stack) va al log a nivel error.

Conflictos de dominio (409)

Dominios y remitentes

domain_already_exists

El dominio ya existe en la instancia.

domain_not_verified

El dominio aún no tiene las verificaciones DNS aprobadas.

domain_sender_not_verified

El dominio del remitente no está verificado.

sender_already_exists

Ya existe un remitente con ese email.

Plantillas

template_not_public

La plantilla referida no es pública.

Credenciales y sandbox

api_key_scope_required

access_type=scoped sin ningún permiso asignado.

sandbox_relay_restricted

Un remitente sandbox intentó enviar a destinatarios fuera de sandbox.

Usuarios

user_already_member

El 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_exists

Ya existe un campo activo con ese field_key (#732).

field_key_reserved

El field_key perteneció a un campo eliminado; queda reservado para siempre y no puede reutilizarse (#732).

field_is_system

Se intentó eliminar un campo de sistema (#732).

dataset_import_field_id_required

Una columna del import está mapeada (action=map) sin field_id (#732).

dataset_import_field_not_found

Una columna del import mapea a un field_id inexistente (#732).

Consentimiento

consent_term_inactive

Se intentó registrar un consentimiento contra un término desactivado (#732, API pública).

consent_channel_not_covered

Un í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_version

El término no tiene ninguna versión vigente (is_current) contra la cual registrar el consentimiento (#732, API pública).

consent_contact_type_channel_mismatch

El 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_rule

Una misma llamada trae dos reglas para el mismo par canal + finalidad (#824, API pública).

consent_contact_pairs_mismatch

Al 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:

CampoSignificado
pointerJSON 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 "".
codeEl 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.
messageMensaje legible en inglés (desde Pydantic). Fallback para integradores; el frontend usa code.
paramsContexto 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):

codeSignificado
missingCampo requerido ausente.
string_too_short / string_too_longFuera del rango de longitud.
string_pattern_mismatchNo cumple el patrón (regex) declarado.
value_errorValidador custom falló (incluye variantes con sufijo, p. ej. value_error.<custom>).
int_parsing / bool_parsing / float_parsingCoerción de tipo falló.
enumValor 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'" }
}
]
}