Appearance
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.sendableyEmailAddress.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_blastque 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/providersyEmailProvider.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.