Saltar al contenido principal

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 code nombra la regla incumplida (sandbox_relay_restricted, field_key_already_exists, ...).
  • 404 por un recurso inexistente: el code es <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​

codeCuándo ocurre
domain_sender_not_verifiedEl dominio del remitente no está verificado.
sender_type_mismatchSe 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_publicLa plantilla no está publicada y el envío no lleva test_mode: true.
template_publishedSe intentó editar una plantilla publicada. Para cambiarla, despublíquela desde el panel.
template_read_onlySe intentó editar una plantilla de un editor que ya no se ofrece: se puede leer y enviar, pero no modificar.
template_name_takenYa 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_unavailableSe intentó crear una plantilla por API en un entorno donde el editor de la plataforma no está disponible.
sandbox_relay_restrictedUn remitente sandbox intentó enviar a destinatarios fuera de sandbox.
sender_is_bulkSe 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_bulkUn 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_existsYa existe un campo activo con ese field_key.
field_key_reservedEl field_key perteneció a un campo eliminado; queda reservado para siempre y no puede reutilizarse.
field_name_takenYa 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_requiredEl 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_takenYa 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_missingEl 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_missingEl 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_declaredEl contacto, la corrección o la búsqueda nombra una columna que el dataset no declara.
dataset_field_not_foundSe declaró como columna un campo que no existe en el catálogo.
dataset_field_duplicatedLa declaración de columnas trae dos veces el mismo campo.
dataset_identity_field_missingLa declaración de columnas deja fuera el campo por el que el dataset reconoce a un contacto.
form_dataset_columns_not_collectedEl 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_formsEl 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_takenYa 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_takenYa 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_duplicatedLa declaración de columnas de una lista trae dos veces el mismo campo.
list_field_position_duplicatedLa declaración de columnas de una lista asigna la misma posición del CSV a dos campos.
list_field_not_foundSe declaró como columna de una lista un campo que no existe en el catálogo.
list_field_inactiveSe 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_dataLa 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_dataUna 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_campaignUna 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_requiredLas 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_missingA 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_snapshotLa 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_progressLa 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_progressHay 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_copiedLa 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_completedSe pidió el archivo de un trabajo que todavía no terminó. Siga el trabajo hasta que su status sea completed.
consent_term_inactiveSe intentó registrar un consentimiento contra un término desactivado.
confirmation_requires_emailUn 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_versionEl término no tiene ninguna versión vigente (is_current) contra la cual registrar el consentimiento.
consent_contact_type_channel_mismatchEl 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_ruleUna misma llamada trae dos reglas para el mismo par canal + finalidad.
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.
contact_objection_existsSe 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_existsLa 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-data supera 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:

detailEndpointQué pasó
Mail ingest temporarily unavailable.POST /v1/mails/send, POST /v1/mails/send_bulkEl 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.cualquieraUna 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 datasetLa 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:

CampoSignificado
pointerJSON Pointer (RFC 6901) al campo, por ejemplo /recipients/0/email. Para un error en la raíz del body, vale "".
codeEl tipo de error (missing, string_too_short, ...). Es estable: sirve como clave para traducir el mensaje, junto con params.
messageMensaje legible en inglés, de respaldo.
paramsContexto 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:

codeSignificado
missingCampo requerido ausente.
string_too_short / string_too_longFuera del rango de longitud.
string_pattern_mismatchNo cumple el patrón (regex) declarado.
value_errorFalló una validación propia del campo (incluye variantes con sufijo, p. ej. value_error.<custom>).
int_parsing / bool_parsing / float_parsingEl valor no se pudo convertir al tipo esperado.
enumValor fuera del conjunto permitido.
field_value_invalidEl 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'" }
}
]
}