Skip to content

Canal de email ​

English version: Email channel.

El email está disponible en core v0.97.0 (openapi/SOURCE.json). Es un canal saliente gobernado: una integración registra direcciones y su procedencia, usa plantillas aprobadas, y lanza una campaña email_blast o se apoya en un efecto de disposición preconfigurado. No existe un endpoint de envío directo POST /v1/email/messages. Contrato: POST|GET /v1/email/addresses, GET /v1/email/messages, POST /v1/email-templates y CampaignWrite.campaign_type.

El canal arranca apagado

GET /v1/me devuelve capabilities.email_channel. Es true sólo cuando la bandera email_channel_enabled del tenant está activa; false es el valor por defecto y significa que el emisor y el ejecutor de email_blast no escriben ninguna fila en el libro ni llaman a ningún proveedor. Una clave ausente significa que el core desplegado es anterior a la capacidad, no que el canal esté disponible. Contrato: GET /v1/me → capabilities.email_channel.

Registrar una dirección con su procedencia ​

Crea una dirección con POST /v1/email/addresses. Los campos obligatorios son contact_id, address, provenance y obtained_at. Todos los tipos de procedencia salvo imported_unverified exigen además un provenance_evidence no vacío; creditor_notice exige también notice_sent_at. Una dirección imported_unverified se guarda pero nunca es enviable. Contrato: POST /v1/email/addresses, campos de la petición provenance, provenance_evidence y notice_sent_at.

Los clientes no pueden marcar una dirección como enviable. Core calcula sendable e ineligible_reason cuando se lee la dirección. Las razones cerradas incluyen evidencia ausente, un periodo de aviso abierto, un dominio no público, una supresión, un estado de supresión ilegible y una dirección borrada (address_erased). Contrato: EmailAddress.sendable y EmailAddress.ineligible_reason.

Tras un borrado de privacidad de la persona, la dirección ya no se puede leer: address vuelve como null e ineligible_reason como address_erased, así que la dirección nunca es enviable. No des por hecho que address es una cadena al listar o leer direcciones. La consola muestra esas filas como «Dirección borrada». Contrato: EmailAddress.address y EmailAddress.ineligible_reason. Véase borrar a una persona.

json
{
  "contact_id": "00000000-0000-4000-8000-000000000001",
  "address": "[email protected]",
  "provenance": "consumer_consent",
  "provenance_evidence": "Written consent recorded in the account file",
  "obtained_at": "2026-10-01T12:00:00Z"
}

Usa GET /v1/email/addresses?contact_id=... para listar las direcciones de un contacto y GET /v1/email/addresses/{id} para leer la elegibilidad actual de una dirección. Contrato: GET /v1/email/addresses y GET /v1/email/addresses/{id}.

Supresión y cese de comunicaciones (cease-and-desist) ​

GET /v1/email/suppressions?address=... devuelve el historial de supresiones, de sólo-anexar, de una dirección normalizada y el estado resultante, suppressed o clear. Gana la última acción del libro. A propósito no hay ruta de escritura en /v1: el procesamiento de bajas, rebotes y quejas escribe las supresiones, mientras que levantar una exige la procedencia propia del consumidor. Contrato: GET /v1/email/suppressions, campos de la respuesta status y email_suppressions.

Un cease-and-desist escrito en el expediente del consumidor es un registro a nivel de consumidor, no una fila de supresión: la puerta de envío bloquea el email a todas las direcciones de ese consumidor, y la dirección misma puede seguir leyéndose con sendable: true. Contrato: GET /v1/consumers/{id} → ConsumerCard.cease_and_desist.

Por separado, una dirección puede llevar una fila de supresión cuyo reason es cease_communication; las demás razones son unsubscribe, hard_bounce, complaint y third_party. Una dirección suprimida se lee como EmailAddress.ineligible_reason: suppressed. Contrato: EmailSuppression.reason y EmailAddress.ineligible_reason.

Las puertas fallan cerradas ​

Antes de cualquier envío, trata todo lo siguiente como requisitos independientes:

  • La capacidad debe estar presente y en true; si no, el canal no hace nada. Contrato: GET /v1/me → capabilities.email_channel.
  • La dirección debe leerse con sendable: true; un estado de supresión ilegible es en sí mismo una razón de inelegibilidad. Contrato: EmailAddress.sendable y EmailAddress.ineligible_reason.
  • No debe haber un cease-and-desist escrito en el expediente del consumidor; bloquea todas las direcciones de ese consumidor, sea cual sea lo que la dirección lea. Contrato: GET /v1/consumers/{id} → ConsumerCard.cease_and_desist.
  • La versión de la plantilla debe estar aprobada. Un email_blast que nombre una versión ajena o no aprobada se rechaza. Contrato: CampaignWrite.email_template_id.
  • Debe resolverse una fila de proveedor activa. No hay proveedor de respaldo; sin uno, no se envía nada. Contrato: POST /v1/email/providers y EmailProvider.status.

Por tanto, capabilities.email_channel: true es permiso para intentar, no una señal de que todo esté listo. Contrato: GET /v1/me → capabilities.email_channel.

Plantillas y aprobación ​

POST /v1/email-templates crea un borrador. El asunto es estático; el cuerpo usa marcadores declarados. El cuerpo estático ya debe contener el aviso de baja y la declaración del cobrador. Crear otro borrador con el mismo nombre inicia la versión siguiente en lugar de editar una versión aprobada. Contrato: POST /v1/email-templates y EmailTemplateInput.

PATCH /v1/email-templates/{id} edita sólo un borrador. Se aprueba con POST /v1/email-templates/{id}/approve; sólo un login account_admin puede hacerlo, y la aprobación registra approved_by y approved_at. Retirar con POST /v1/email-templates/{id}/retire es terminal y saca esa versión de uso. Contrato: PATCH /v1/email-templates/{id}, los campos EmailTemplate.approved_by y EmailTemplate.approved_at, y las rutas approve/retire.

Enviar tras una disposición ​

Registrar POST /v1/call_attempts/{id}/disposition confirma primero el resultado y después lo entrega a la política de disposiciones del tenant. Una regla almacenada es visible mediante GET /v1/disposition-rules; cuando su effect es send_email, el runtime usa su plantilla y sus variables a través del mismo emisor con puertas que cualquier otro email. Contrato: POST /v1/call_attempts/{id}/disposition y DispositionRule.effect/DispositionRule.params.

El PUT /v1/disposition-rules público sólo puede escribir reschedule y suppress. No puede aprovisionar send_email; las integraciones no deben fabricar ese efecto ni presentarlo como un ajuste de autoservicio. Contrato: DispositionRulesWrite.rules[].effect.

Ejecutar una campaña email_blast ​

Crea la campaña con campaign_type: "email_blast", un email_template_id aprobado y valores en email_template_variables. Al iniciarla con POST /v1/campaigns/{id}/start se selecciona el ejecutor de email, manteniendo el mismo ciclo de vida de campaña que los demás tipos de canal. Contrato: POST /v1/campaigns, CampaignWrite.email_template_id, CampaignWrite.email_template_variables y POST /v1/campaigns/{id}/start.

Lee el libro resultante con GET /v1/email/messages o la vista colapsada en GET /v1/email/messages/{id}. Para una deuda, usa GET /v1/debts/{id}/messages?channel=email. Contrato: EmailMessage.status, EmailMessageView.transitions y GET /v1/debts/{id}/messages.

Eventos firmados del proveedor ​

Los callbacks del proveedor usan una llave HMAC por proveedor. Se entrega como webhook_secret, de sólo escritura, al registrar la fila; las lecturas exponen únicamente has_webhook_secret. Rotar el valor reemplaza la llave, y borrarlo hace que esa fila deje de aceptar eventos. Contrato: POST /v1/email/providers → webhook_secret, PATCH /v1/email/providers/{id} → webhook_secret y EmailProvider.has_webhook_secret.

Los eventos aceptados aparecen como transiciones del libro, tales como delivered, deferred, bounced y complained; el procesamiento de rebotes y quejas también puede anexar la razón de supresión correspondiente. Contrato: EmailMessage.status y EmailSuppression.reason.

El contrato nombra los adaptadores genéricos smtp y log. No promete ningún proveedor de email en la nube en concreto. Contrato: EmailProvider.adapter.

Baja de un clic según RFC 8058 ​

La acción de un clic es propiedad de la plataforma, no una escritura de supresión en /v1 que una integración deba recrear. Toda plantilla ya debe contener el aviso de baja, y el resultado visible en el contrato de una baja es una supresión con reason: unsubscribe. El contrato /v1 ofrece de forma explícita sólo la ruta de lectura de ese libro. Contrato: POST /v1/email-templates, EmailSuppression.reason y GET /v1/email/suppressions.

No levantes esa supresión sólo porque cambie una campaña o una plantilla. Un levantamiento es otra acción de sólo-anexar en el libro y exige la procedencia propia del consumidor. Contrato: EmailSuppression.action, EmailSuppression.provenance y GET /v1/email/suppressions.

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.