Errores genéricos
Para desarrolladores
Los códigos de error comunes a toda la API, con el status HTTP y el significado de cada uno. Dos casos llevan un code específico en lugar del genérico:
- 409 por una regla de negocio: el
codenombra la regla incumplida (sandbox_relay_restricted,field_key_already_exists, ...). - 404 por un recurso inexistente: el
codees<recurso>_not_found.
Sumar códigos nuevos o más específicos no rompe una integración existente. Reemplazar un código específico por uno más genérico, en cambio, sí la rompería.
bad-request
code: bad_request · HTTP 400. Request sintácticamente válido pero rechazado antes de la validación de esquema (p. ej. parámetro de query mal formado). El detail es descriptivo.
unauthorized
code: unauthorized · HTTP 401. Falta el Authorization, el token es inválido o expiró, o la API key es desconocida/revocada. detail es genérico ("Authentication required.") y no distingue token inválido de expirado.
Un X-Instance-Slug que no corresponde a ninguna instancia también produce un 401: la respuesta es idéntica en status, body y headers a la de una API key inválida, de modo que no revela qué slugs existen. Si recibe un 401 inesperado, revise tanto la API key como el slug.
Todo 401 incluye el header WWW-Authenticate: Bearer, sin los parámetros realm ni error (RFC 6750 §3.1). Si necesita que soporte revise el caso, indique el X-Request-Id de la respuesta.
forbidden
code: forbidden · HTTP 403. El request está autenticado pero no autorizado: el scope es insuficiente, el rol no corresponde o la instancia de destino está deshabilitada. Una API key nunca opera una instancia deshabilitada. El detail es genérico ("Access denied.") y no informa el motivo. La denegación por dirección de origen tiene su propio code (ver ip-not-allowed).
ip-not-allowed
code: ip_not_allowed · HTTP 403. La dirección de origen del request no está permitida. Lo emiten los dos controles de IP: la lista de IPs permitidas de la instancia y la de la API key.
El detail es el mismo texto genérico de cualquier 403 ("Access denied."): solo el code lo distingue. No se informa cuál de los dos controles rechazó el request, ni las direcciones o rangos configurados.
Solo lo recibe un request ya autenticado y autorizado sobre la instancia. Un request sin credencial válida, aunque venga de una dirección no permitida, recibe el 401 o 403 genérico que corresponda, nunca este código. Así la respuesta no revela que la instancia existe ni que tiene una lista de IPs permitidas.
Qué hacer al recibirlo: revise desde qué dirección sale el tráfico y compárela con la lista de IPs permitidas de la instancia y con la de la API key en uso. Si el tráfico sale por un NAT o un proxy, el control evalúa la dirección pública de salida, no la interna del host.
not-found
code: <recurso>_not_found · HTTP 404. El recurso no existe. El code nombra el recurso (domain_not_found para un dominio, sender_not_found para un remitente) y su type termina en <recurso>-not-found. El detail es descriptivo: "{recurso} '{id}' not found". El code genérico not_found solo aparece ante una ruta inexistente.
mail-outside-retention
code: mail_outside_retention · HTTP 404. Lo devuelven GET /v1/mails/{mail_id} y GET /v1/mails/{mail_id}/logs cuando el identificador corresponde a una fecha anterior a la ventana de retención en línea (3 meses). El correo puede haber existido, pero ya no está disponible. Ante mail_not_found, revise el id. Ante este código, el id es válido pero demasiado antiguo para consultarlo.
method-not-allowed
code: method_not_allowed · HTTP 405. El path existe pero no acepta el verbo HTTP usado.
conflict
code: <regla> · HTTP 409. Violación de unicidad o de una regla de negocio. El code siempre es específico y nombra la regla incumplida. El genérico conflict no se emite.
Códigos más comunes
code | Cuándo ocurre |
|---|---|
domain_sender_not_verified | El dominio del remitente no está verificado. |
template_not_public | La plantilla referida no es pública y el envío no es de prueba (test_mode: true). |
template_published | Se intentó editar una plantilla publicada. Para cambiarla, despublíquela desde el panel. |
template_read_only | Se intentó editar una plantilla de un editor que ya no se ofrece: se puede leer y enviar, pero no modificar. |
template_name_taken | Ya existe otra plantilla con ese nombre. No importan mayúsculas ni tildes (Confirmación de compra y confirmacion de compra son el mismo nombre), y el de una plantilla eliminada queda libre. |
template_editor_unavailable | Se intentó crear una plantilla por API en un entorno donde el editor de la plataforma no está disponible. |
sandbox_relay_restricted | Un remitente sandbox intentó enviar a destinatarios fuera de sandbox. |
field_key_already_exists | Ya existe un campo activo con ese field_key. |
field_key_reserved | El field_key perteneció a un campo eliminado; queda reservado para siempre y no puede reutilizarse. |
field_name_taken | Ya existe otro campo con ese nombre. No importan mayúsculas ni tildes (Región y region son el mismo nombre), y el de un campo eliminado queda libre. |
dataset_identity_field_required | El dataset declara más de un canal, o ninguno, así que el canal no determina por cuál identificador reconoce a un contacto y hay que declararlo. |
dataset_name_taken | Ya existe otro dataset con ese nombre. No importan mayúsculas ni tildes (Clientes región y clientes region son el mismo nombre), y el de un dataset eliminado queda libre. |
dataset_identity_value_missing | El contacto no trae el identificador por el que ese dataset reconoce a un contacto. Una identidad parcial es válida; una ausente no. |
dataset_required_column_missing | El contacto no trae una columna que el dataset declara obligatoria. |
dataset_column_not_declared | El contacto, la corrección o la búsqueda nombra una columna que el dataset no declara. |
dataset_field_not_found | Se declaró como columna un campo que no existe en el catálogo. |
dataset_field_duplicated | La declaración de columnas trae dos veces el mismo campo. |
dataset_identity_field_missing | La declaración de columnas deja fuera el campo por el que el dataset reconoce a un contacto. |
form_dataset_columns_not_collected | El formulario no recolecta como ítem obligatorio una columna que exige el dataset que alimenta. Ocurre al apuntarlo al dataset sin pedir que se complete (align_dataset_columns), o al quitar o volver opcional el último ítem obligatorio que recolectaba esa columna. El detalle nombra las columnas. |
dataset_columns_not_collected_by_forms | El cambio vuelve obligatoria una columna que un formulario conectado al dataset no recolecta como obligatoria, y no se indicó qué hacer con esos formularios (connected_forms: align o keep). El detalle nombra cada formulario y lo que le falta. |
form_name_taken | Ya existe otro formulario con ese nombre. No importan mayúsculas ni tildes (Suscripción newsletter y suscripcion newsletter son el mismo nombre), y el de un formulario eliminado queda libre. |
list_name_taken | Ya existe otra lista con ese nombre. No importan mayúsculas ni tildes (Clientes Black Friday y clientes black friday son el mismo nombre), y el de una lista eliminada queda libre. |
list_field_duplicated | La declaración de columnas de una lista trae dos veces el mismo campo. |
list_field_position_duplicated | La declaración de columnas de una lista asigna la misma posición del CSV a dos campos. |
list_field_not_found | Se declaró como columna de una lista un campo que no existe en el catálogo. |
list_field_inactive | Se declaró como columna de una lista un campo desactivado del catálogo. |
list_has_no_fields | Se subió un archivo a una lista que no tiene columnas declaradas: sin ellas no hay cómo interpretar el archivo. |
list_size_exceeded | La lista superaría su tamaño máximo con el archivo entrante. Se mide sobre los datos que ya tiene más el archivo; un overwrite cuenta solo el archivo. |
list_import_duplicated | En un append, el archivo es idéntico a uno que la lista ya tiene, o a uno cuya importación sigue en curso. El detail nombra esa importación. |
consent_term_inactive | Se intentó registrar un consentimiento contra un término desactivado. |
consent_term_no_current_version | El término no tiene ninguna versión vigente (is_current) contra la cual registrar el consentimiento. |
consent_contact_type_channel_mismatch | El tipo de identificador no es válido para el canal de la regla. Por ejemplo, una dirección de correo en un canal push, o un teléfono en mail. |
consent_duplicate_rule | Una misma llamada trae dos reglas para el mismo par canal + finalidad. |
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. |
request-too-large
code: request_too_large · HTTP 413. El request supera alguno de estos límites de tamaño:
- El body del request supera el máximo permitido para el path.
- El body llega comprimido y supera el máximo al descomprimirlo.
- Un adjunto individual de
multipart/form-datasupera su límite. - El archivo de una importación de lista 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.
Si el rechazo viene del límite del body, la respuesta incluye max_bytes.
Los límites concretos están en Enviar un correo → Límites de tamaño.
unsupported-media-type
code: unsupported_media_type · HTTP 415. El Content-Type enviado no es el esperado (típicamente, no es JSON).
validation-error
code: validation_error · HTTP 422. Falla de validación de esquema. El body incluye errors[] (ver Errores por campo (422)).
rate-limit-exceeded
code: rate_limit_exceeded · HTTP 429. Se superó el límite de ancho de banda de la instancia. La respuesta incluye el header Retry-After (segundos a esperar antes de reintentar).
service-unavailable
code: service_unavailable · HTTP 503. Un servicio del que depende la operación no respondió. El error es transitorio: reintente. El code es siempre el mismo. El detail nombra el servicio cuando eso le sirve a quien llama, y es genérico cuando no:
detail | Endpoint | Qué pasó |
|---|---|---|
Mail ingest temporarily unavailable. | POST /v1/mails/send | El servicio no pudo aceptar el correo tras agotar los reintentos. |
Service temporarily unavailable. | cualquiera | Una dependencia interna no respondió. |
The search took too long; narrow it down. | búsqueda de contactos de un dataset | La búsqueda recorrió demasiados contactos. No es un error de la petición: acótela con un valor más preciso, o busque por el identificador exacto o por cómo empieza. |
internal-error
code: internal_error · HTTP 500. Error no manejado. El detail es un texto genérico ("Internal server error.").
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, por ejemplo /recipients/0/email. Para un error en la raíz del body, vale "". |
code | El tipo de error (missing, string_too_short, ...). Es estable: sirve como clave para traducir el mensaje, junto con params. |
message | Mensaje legible en inglés, de respaldo. |
params | Contexto del error; solo presente cuando no está vacío. Sus valores (p. ej. { "min_length": 3 }) sirven para completar el mensaje traducido. Los valores que no son números ni textos simples llegan como texto. |
Valores frecuentes de errors[].code:
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 | Falló una validación propia del campo (incluye variantes con sufijo, p. ej. value_error.<custom>). |
int_parsing / bool_parsing / float_parsing | El valor no se pudo convertir al tipo esperado. |
enum | Valor fuera del conjunto permitido. |
field_value_invalid | El valor de un contacto no cumple las reglas de su campo del catálogo (opciones, largo, patrón o rango). El pointer es /values/<field_key>. Lo emiten la carga de contactos de un dataset y la corrección de un contacto. |
Algunos endpoints agregan valores propios de errors[].code para reglas de su dominio (por ejemplo, invalid_document o essential_not_supported en las plantillas); cada ficha de endpoint los lista en su sección de errores.
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'" }
}
]
}