Errores genéricos
Para desarrolladores
Los códigos de error que puede devolver cualquier endpoint de 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 rechazado antes de la validación de esquema, por ejemplo por un parámetro de query mal formado. El detail es descriptivo.
idempotency-key-required
code: idempotency_key_required · HTTP 400. El endpoint exige el header Idempotency-Key y el request no lo trae. Hoy lo exige POST /v1/mails/send_bulk: un lote puede llevar miles de destinatarios, y sin clave un reintento tras un timeout los enviaría todos de nuevo. El request se rechaza sin leer el body; repítalo con el header.
unauthorized
code: unauthorized · HTTP 401. Falta el Authorization, el token es inválido o expiró, o la API key es desconocida/revocada. El 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 un path 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. |
sender_type_mismatch | Se intentó crear un remitente con la dirección de un remitente eliminado del otro tipo (transaccional o masivo). Una dirección conserva para siempre el tipo con que se creó; para el otro tipo, use otra dirección. |
template_not_public | La plantilla no está publicada y el envío no lleva 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. |
sender_is_bulk | Se intentó enviar con un remitente masivo por POST /v1/mails/send, o asignarlo a la respuesta automática y la confirmación de un formulario. Un remitente masivo envía solo por POST /v1/mails/send_bulk, y un formulario necesita un remitente transaccional. |
sender_not_bulk | Un mensaje de un lote de POST /v1/mails/send_bulk usa un remitente transaccional, que envía por POST /v1/mails/send. El detail indica qué mensaje. |
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, o la lista que se copia al dataset no tiene esa columna. El detail nombra las columnas. |
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 detail 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 detail 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, o una de las listas que se combinan tiene una columna de un campo desactivado. |
list_has_data | La lista ya recibió sus datos, de un archivo, de una combinación o de una instantánea de un dataset. Una lista se carga una sola vez, y desde entonces no admite otro archivo ni cambios en sus columnas o en repair_encoding: un archivo nuevo va en una lista nueva. |
list_has_no_data | Una lista todavía no tiene datos: una de las que se quieren combinar, la que se quiere copiar a un dataset o la de una campaña que se programa. |
list_used_by_campaign | Una campaña programada, en curso o pausada envía a la lista, así que no se puede eliminar, ni eliminarla al combinarla con delete_sources. Se libera cuando la campaña termina o se cancela. |
list_identity_field_required | Las listas que se combinan tienen más de un canal, o ninguno, así que el canal no determina por cuál identificador se reconoce a un contacto y hay que indicarlo en identity_field. |
list_identity_field_missing | A una de las listas que se combinan, o al dataset del que se toma una instantánea, le falta la columna del campo que identifica a un contacto. Al copiar una lista a un dataset, a la lista le falta la columna por la que ese dataset reconoce a un contacto. |
list_is_snapshot | La lista salió de una instantánea de un dataset y solo recibe datos de él: no admite un archivo ni cambios en sus columnas o en repair_encoding, aunque su instantánea haya fallado. Para tener los contactos de nuevo, se toma otra instantánea. |
list_import_in_progress | La lista tiene una importación pendiente o en curso. Mientras tanto no admite otro archivo ni cambios en sus columnas o en repair_encoding. El detail nombra esa importación. |
list_copy_in_progress | Hay otra copia de una lista en curso en la instancia, hacia ese dataset o hacia otro. Las copias se ejecutan de una en una. Se rechaza aunque el pedido lleve force. El detail nombra el trabajo en curso. |
list_already_copied | La lista ya se copió a ese dataset, con los mismos datos. Se copia de nuevo enviando force: true. El detail nombra el trabajo de la copia anterior. |
job_not_completed | Se pidió el archivo de un trabajo que todavía no terminó. Siga el trabajo hasta que su status sea completed. |
consent_term_inactive | Se intentó registrar un consentimiento contra un término desactivado. |
confirmation_requires_email | Un formulario quedaría con doble confirmación sin un campo Email marcado como obligatorio, y no habría destino donde enviar el correo de confirmación. Se rechaza al activarlo y, si ya está activo, al activar la doble confirmación, al volver opcional ese Email o al quitarlo. |
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. |
contact_objection_exists | Se intentó registrar una oposición, con POST /v1/mail-restrictions o en el panel, y las oposiciones vigentes de la dirección, sumadas, ya cubren todos los tipos de envío pedidos. Una oposición que agrega un tipo de envío que ninguna cubre sí se registra. |
contact_block_exists | La dirección ya tiene un bloqueo temporal vigente: solo puede haber uno por dirección. |
idempotency-key-in-progress
code: idempotency_key_in_progress · HTTP 409. Otro request con la misma Idempotency-Key sigue en curso. Lo habitual es que sea un reintento que llegó mientras la llamada original todavía enviaba su lote. La respuesta incluye el header Retry-After (segundos). Espere ese plazo y repita la llamada con la misma clave y el mismo body: recibirá la respuesta de la original, o completará el lote si esa se interrumpió. Si la original se interrumpió sin poder registrarlo, este código puede repetirse durante hasta un minuto. Lo emite POST /v1/mails/send_bulk.
job-file-expired
code: job_file_expired · HTTP 410. El archivo que dejó un trabajo, como el CSV de una exportación de lista, venció y ya no se puede descargar (GET /v1/jobs/{id}/file). Repetir la descarga no lo trae de vuelta: pida una exportación nueva. A diferencia de job_file_not_found (404), que dice que el trabajo no dejó archivo, este código dice que lo dejó y ya no está. El code genérico de un 410, gone, no se emite.
reply-content-expired
code: reply_content_expired · HTTP 410. El contenido de una respuesta a un correo (su asunto, su texto y sus adjuntos) se guarda 30 días desde que llegó, y este ya no está (GET /v1/mails/{mail_id}/replies/{reply_id} y la descarga de sus adjuntos). La respuesta sigue en el listado con sus datos. Repetir la consulta no lo trae de vuelta: para conservarlo, léalo cuando llegue el evento mail.replied.
reply-mailbox-address-invalid
code: reply_mailbox_address_invalid · HTTP 422. La dirección de un buzón de respuestas no tiene forma de dirección de correo.
reply-mailbox-platform-domain
code: reply_mailbox_platform_domain · HTTP 422. El dominio de la dirección es de la instancia, o su MX apunta a la plataforma: el reenvío volvería a la plataforma, que no entrega correo a esos dominios. Use una dirección de otro dominio.
reply-mailbox-code-recently-sent
code: reply_mailbox_code_recently_sent · HTTP 409. Se envió un código de confirmación hace menos de un minuto. Reintente pasado ese tiempo.
reply-mailbox-code-invalid
code: reply_mailbox_code_invalid · HTTP 422. El código no es el que recibió el buzón. Cuenta como un intento de los 5 que admite.
reply-mailbox-code-expired
code: reply_mailbox_code_expired · HTTP 409. El código venció: vale 15 minutos. Guardar la dirección de nuevo envía otro.
reply-mailbox-code-attempts-exhausted
code: reply_mailbox_code_attempts_exhausted · HTTP 409. Se agotaron los 5 intentos. Guardar la dirección de nuevo envía otro código.
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.
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 y, para el lote, en POST /v1/mails/send_bulk → Límites de tamaño.
unsupported-media-type
code: unsupported_media_type · HTTP 415. El Content-Type enviado no es el esperado (por lo general, 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)).
idempotency-key-reused
code: idempotency_key_reused · HTTP 422. La Idempotency-Key ya se usó con un request distinto. No lleva errors[], porque el body no tiene nada que corregir: repita el request con una clave nueva. Una clave repetida suele deberse a un contador que se reinició. Por eso se rechaza en lugar de devolver la respuesta anterior, que haría perder el request nuevo sin aviso. Lo emiten POST /v1/forms/{id}/submissions, donde la clave vale para el formulario, y POST /v1/mails/send_bulk, donde vale para la instancia durante 24 horas.
rate-limit-cost-exceeds-quota
code: rate_limit_cost_exceeds_quota · HTTP 422. El envío tiene más destinatarios que el cupo de envío entero de la API key, así que ninguna espera lo deja pasar. No lleva errors[]: para enviarlo, divídalo en envíos con menos destinatarios o amplíe el cupo de la API key. Lo emiten POST /v1/mails/send y POST /v1/mails/send_bulk; en un lote, el detail indica el mensaje y no se envía ninguno.
rate-limit-exceeded
code: rate_limit_exceeded · HTTP 429. El envío no cabe ahora en uno de sus límites: el cupo de envío de la API key o un límite de la instancia. La respuesta incluye el header Retry-After (segundos a esperar antes de reintentar).
Cuando el límite es un cupo de envío, el body lleva además limit_scope (credential para el cupo de la API key, instance para un límite de toda la instancia, como la tasa del plan) y la respuesta incluye los headers X-RateLimit-*. La tasa transaccional del plan no rechaza un envío que puede salir dentro de 30 minutos: lo acepta y lo demora.
También lo emite POST /v1/datasets/{id}/contacts cuando el dataset superó su tasa de ingreso, con limit_scope: dataset y los mismos headers.
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, POST /v1/mails/send_bulk | El servicio no pudo aceptar el correo tras agotar los reintentos. En un lote, los mensajes ya aceptados quedan registrados, y repetir la llamada con la misma Idempotency-Key envía el resto. |
Service temporarily unavailable. | cualquiera | Una dependencia interna no respondió. A veces la respuesta trae además Retry-After: espere ese plazo antes de reintentar. |
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 del request: 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, o pattern_unsupported en los campos); cada ficha de endpoint los lista en su sección de errores.
Registrar un bloqueo temporal, con POST /v1/mail-restrictions o en el panel, agrega cuatro valores en /expires_at:
expires_at_not_future: la fecha de término no es futura.expires_at_out_of_range: la fecha de término es posterior al 19-01-2038.expires_at_block_only: se indicó una fecha de término en una oposición.expires_at_nonexistent: la fecha de término no trae zona horaria y cae en una hora que el cambio de horario se salta.
Un asunto, un preheader o un diseño que no es una plantilla Liquid válida se rechaza con template_syntax_error al enviar con vars. Al guardar una plantilla, el asunto y el preheader responden ese mismo código. Un error en el diseño llega como invalid_document, con template_syntax_error en params.rule. La vista previa de una plantilla con valores de ejemplo agrega template_render_failed en /vars, cuando la plantilla no se puede completar con ellos.
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'" }
}
]
}