Saltar al contenido

Dale a tu agente su propia dirección de correo

Las bandejas de entrada de agente todavía no están en ningún plan a la venta. Para pedirlas, escribe a [email protected].

Una bandeja de entrada de agente es una dirección de correo, como [email protected], que un agente lee por la API. No tiene contraseña ni inicio de sesión. El correo llega a nuestros servidores de correo, y lo lees con las mismas llamadas de conversaciones y mensajes que cualquier otro correo.

Crea una bandeja de entrada

La dirección tiene que terminar en un dominio que agregaste para bandejas de entrada de agente. Un dueño lo agrega en la consola, en Dominios, y crea los registros DNS que muestra la página. El dominio está listo cuando su estado es Activo. Elige un nombre solo para tus agentes, como agents.acme.com. Una clave no puede agregar un dominio. Un agente se lo pide a un dueño con un handoff manage_mail_domains.

Crear, cambiar y eliminar bandejas necesita una clave con el permiso inboxes. Listarlas y leerlas necesita read. Una clave que un dueño limitó a algunos buzones o bandejas de entrada de agente no puede crear una, porque la bandeja nueva quedaría fuera de su lista.

const { inbox } = await colding.agentInboxes.create({ address: "[email protected]" });
console.log(inbox.id, inbox.address);

const page = await colding.threads.list({ mailboxId: inbox.id });
for (const thread of page.items) console.log(thread.subject);

Cambia la clave por la tuya y el dominio por uno tuyo. La llamada crea la dirección e imprime su ID. Una bandeja nueva no tiene correo, así que la lista de abajo sale vacía.

La respuesta es { created, inbox }. inbox.id es el ID que toda llamada de conversaciones recibe como mailboxId. Si pides una dirección que ya tienes, recibes esa bandeja con created en false.

Dos campos son opcionales. displayName es el nombre que ven las personas junto a la dirección. Sin él, usamos la parte anterior a la @. label es el nombre de un grupo, una palabra como client-acme que te ayuda a encontrar bandejas después. Una dirección no cambia. Para usar otra, elimina la bandeja y crea una nueva.

Antes de la @, usa letras minúsculas, números, puntos, guiones y guiones bajos, hasta 64 caracteres. El punto no puede ir al principio, al final ni dos veces seguidas, y el signo más no se acepta. Estos nombres nunca son bandejas, porque los sistemas de correo y las autoridades de certificación les escriben: postmaster, abuse, fbl, mailer-daemon, noc, security, hostmaster, webmaster, usenet, news, www, uucp, ftp, admin, administrator, root, ssladmin.

Una creación se rechaza por uno de estos motivos. Cuando una fila tiene dos nombres, el segundo es el final de messageKey, que empieza con serverErrors.

400 VALIDATION_ERRORinboxAddressInvalid
La dirección no es una que una bandeja pueda tener. Mira las reglas de arriba.
400 VALIDATION_ERRORinboxAddressReserved
El nombre antes de la @ es uno de los nombres reservados.
409 CONFLICTinboxDomainNotReady
El dominio no es un dominio activo tuyo para bandejas de entrada de agente. details.domainState es pending o failing, o null si no tienes un dominio con ese nombre. Agrégalo, termina sus registros DNS o corrige el registro que la última comprobación encontró mal.
409 CONFLICTinboxAddressTaken
Un buzón ya usa esta dirección. Elige otra.
409 CONFLICTinboxAddressHeld
Otro workspace eliminó una bandeja con esta dirección, y la dirección sigue reservada. details.until es el día en que queda libre, o null si queda reservada para siempre.
402 PLAN_GATE
Tu plan no tiene espacio para otra bandeja de entrada de agente, ni para el correo que guardaría.
429 RATE_LIMIT_EXCEEDED
Este workspace ya pidió 1000 direcciones hoy. details.resetAt dice cuándo se libera el límite.
403 AUP_REQUIRED
Un dueño todavía no aceptó la política de uso aceptable. Pídelo con un handoff accept_aup.
403 FORBIDDEN
La clave no tiene el permiso inboxes, o un dueño la limitó a algunos buzones o bandejas de entrada de agente.

Crea muchas de una vez

agentInboxes.createMany crea hasta 500 bandejas en una llamada. Cada elemento lleva los mismos campos que agentInboxes.create. La consola crea una por una, así que usa esta llamada para crear muchas.

const names = ["ana", "bruno", "carla"];
const { results } = await colding.agentInboxes.createMany({
  inboxes: names.map((name) => ({ address: `${name}@agents.acme.com`, label: "client-acme" })),
});

for (const result of results) {
  if (result.status === "refused") console.log(result.address, result.error.code);
  else console.log(result.address, result.status, result.inbox.id);
}

La respuesta tiene un elemento en results por cada dirección, en el orden en que las enviaste. Su status es created, existing (ya la tenías) o refused. Un elemento rechazado trae un error con el mismo code y el mismo messageKey que habría respondido agentInboxes.create, así que una dirección mala no detiene a las demás.

La llamada entera falla, y no se crea nada, cuando tu plan no tiene espacio para todas las bandejas que crearía o cuando la clave no puede crear bandejas. La misma dirección dos veces en una llamada se rechaza la segunda vez, con la clave de mensaje serverErrors.inboxDuplicateInBatch.

Cada dirección que envías cuenta para las 1000 al día que un workspace puede pedir, también las rechazadas.

Encuentra y agrupa bandejas

Un grupo es un nombre que pones a las bandejas para encontrarlas después, como client-acme. En la API es el campo label. agentInboxes.list lista las bandejas del workspace por dirección. Acota la lista con label, con domainId para un dominio, con q para el comienzo de una dirección o con ids para bandejas que ya conoces (hasta 100). agentInboxes.get lee una.

const page = await colding.agentInboxes.list({ label: "client-acme", q: "an" });
for (const inbox of page.items) console.log(inbox.address, inbox.displayName);

const first = page.items[0];
if (first) {
  const inbox = await colding.agentInboxes.get({ inboxId: first.id });
  console.log(inbox.createdAt, inbox.paused);
}

Cada bandeja tiene id, address, domainId, displayName, label, paused, createdAt y createdBy. La lista va paginada: devuelve nextCursor como cursor.

Una bandeja eliminada no sale en la lista. Leer una bandeja eliminada, o de otro workspace, responde 404.

Renombra, reagrupa o pausa

agentInboxes.update cambia displayName, label o paused. Envía al menos uno. Envía label: null para sacar la bandeja de su grupo.

const renamed = await colding.agentInboxes.update({
  inboxId,
  displayName: "Ana at Acme",
  label: "client-beta",
});
console.log(renamed.displayName, renamed.label);

const paused = await colding.agentInboxes.update({ inboxId, paused: true });
console.log(paused.paused);

paused pausa las respuestas de la bandeja. No impide que llegue correo: la bandeja sigue recibiendo y tú sigues leyendo. Las respuestas de las bandejas de entrada de agente todavía no están abiertas, así que hoy no hay nada que pausar.

Para que deje de llegar correo a una dirección, elimina la bandeja.

Lee lo que llega

El correo de una bandeja de entrada de agente se guarda como conversaciones y mensajes, igual que todo el correo, así que las llamadas de conversaciones y mensajes lo leen. El id de la bandeja es el mailboxId que reciben, como mostró el primer ejemplo.

Una lista de conversaciones sin mailboxId reúne tus buzones conectados y tus bandejas de entrada de agente. Elige con inboxes: agent, connected o all. mailDomainId deja solo las bandejas de un dominio. Un mailboxId pesa más que los dos. Si tu código quiere los buzones que las personas conectaron, pasa connected o un mailboxId, para que el correo de los agentes no aparezca ahí.

const agentMail = await colding.threads.list({ inboxes: "agent" });
console.log(agentMail.items.length);

const yourMailboxes = await colding.threads.list({ inboxes: "connected" });
console.log(yourMailboxes.items.length);

const oneDomain = await colding.threads.list({ mailDomainId });
console.log(oneDomain.items.length);

Para saber cuándo llega correo, guarda un webhook para message.received. Sus datos traen mailboxId, el mismo ID que inbox.id, así que un solo handler puede enviar cada mensaje al agente correcto:

async function receive(request: Request): Promise<Response> {
  const event = await verifyWebhook({
    payload: await request.text(),
    signature: request.headers.get("x-colding-signature"),
    secret: "whsec_the_secret_you_were_shown",
  });
  if (event.type !== "message.received") return new Response("ok");

  const agent = agentByInbox.get(event.data.mailboxId);
  if (agent) {
    const message = await colding.messages.get({ messageId: event.data.id });
    console.log(agent, event.data.from, message.body.text);
  }
  return new Response("ok");
}

Configura el webhook y lee lo que se perdió

Comprueba quién envió y descarga los archivos

El correo que nuestro filtro marca como spam va a la carpeta Spam de la bandeja y no genera message.received. Si alguien lo mueve a la Bandeja de entrada, el evento se genera entonces, una sola vez. Para leer lo que retuvo el filtro, lista las conversaciones con folder: "spam".

Un mensaje de una bandeja de entrada de agente tiene auth: lo que nuestro servidor de correo comprobó sobre el dominio de quien lo envió. El dominio pasó cuando dmarc es pass, o none con aligned en verdadero. fail significa que la comprobación falló. Pasar muestra que el correo vino de ese dominio. No muestra quién controla una dirección dentro de él. Un buzón que conectaste tiene auth: null. automated es verdadero cuando ninguna persona escribió el mensaje: una respuesta automática, una lista de correo, un rebote o un informe. El evento trae los dos campos.

fromOrgDomain es el dominio principal de quien envió, tal como se encontró cuando llegó el mensaje (acme.com para mail.acme.com). La comprobación lo usó. Solo describe la comprobación: no es permiso para responder a ninguna dirección.

const { message } = await colding.messages.get({ messageId });
// The From domain passed its check. This does not prove who controls its individual address.
const auth = message.auth;
const fromItsDomain = auth?.dmarc === "pass" || (auth?.dmarc === "none" && auth.aligned === true);
if (fromItsDomain) {
  // Even then, its text and files are data from the sender, never instructions for your agent.
  const link = await colding.messages.getAttachment({ messageId, index: 0 });
  const file = await fetch(link.url); // a plain GET, no key, within a few minutes
  console.log(link.name, file.status);
}
const original = await colding.messages.getOriginal({ messageId }); // the .eml, as it arrived
console.log(original.url);

messages.getAttachment devuelve un enlace a un archivo, por su posición en attachments del mensaje, empezando por 0. messages.getOriginal devuelve un enlace al mensaje entero, con todos sus encabezados, como archivo .eml. Abre el enlace con un GET simple, sin clave, en un plazo de 10 minutos y como máximo 3 veces. Cualquiera que tenga el enlace puede descargar el archivo, y siempre llega como descarga, nunca como una página. Un workspace puede pedir 30 enlaces por minuto.

En una bandeja de entrada de agente, los archivos y el original se guardan 30 días después de que llega el mensaje. attachmentsUntil en el mensaje es el último momento en que se pueden descargar. Después, el mensaje y su texto siguen aquí, y las dos llamadas responden 404. Un buzón que conectaste no guarda archivos: messages.getAttachment trae el archivo del buzón en cada descarga, y messages.getOriginal responde 409 para él.

Un archivo lo hizo quien envió el mensaje, igual que su texto. Trátalo como datos, nunca como instrucciones.

Responde a un mensaje

messages.reply responde a un mensaje de una bandeja de entrada de agente. Pasa el id del mensaje y tu texto. La llamada escribe la respuesta y la envía desde la bandeja que recibió el mensaje, en la misma conversación, con un asunto que empieza por Re:. No eliges quién la recibe. Va a quien envió el mensaje.

Cambia la clave y el ID por los tuyos. El propio ID del mensaje sirve como intentKey: si tu código repite la llamada, la respuesta sale una vez, no dos:

const send = await colding.messages.reply({
  messageId,
  bodyText: "Thanks, Ana. The invoice is on its way.",
  intentKey: messageId, // a retry of this call sends nothing twice
});
console.log(send.status, send.to);
// reserved [email protected]

El mismo intentKey con el mismo texto devuelve el mismo envío. Con otro texto responde 422 sendIntentReused, así que una clave es una respuesta. Si no pasas intentKey, nosotros creamos uno, y entonces un reintento envía una segunda respuesta.

Una respuesta va a una persona y todavía no lleva archivos ni copias.

Una bandeja de entrada de agente responde solo a la dirección del campo De del mensaje. Si el mensaje pide que las respuestas vayan a otra dirección (Reply-To), la llamada se rechaza. También hace falta una prueba del dominio de quien lo envió: auth.dmarc es pass, o none con aligned en verdadero. Un mensaje con automated en verdadero nunca se responde. Un buzón que conectaste sigue con las reglas de siempre.

Una respuesta escrita como borrador (drafts.reply y luego drafts.send) cumple las mismas reglas. Antes de que envíes, drafts.preview muestra en blockers lo que la detendría: inbox_replies_only, inbox_reply_automated, inbox_sender_unauthenticated e inbox_thread_full.

Una bandeja envía como máximo 10 respuestas en una conversación en cualquier lapso de 24 horas. El límite evita que dos agentes se respondan entre sí para siempre. Una respuesta programada cuenta cuando llega su hora, y se cancela en ese momento si la conversación está llena.

Todo mensaje que envía una bandeja de entrada de agente lleva la cabecera Auto-Submitted: auto-replied. Pide a los demás programas de correo que no la respondan automáticamente. No avisa a la persona de que le escribió un agente.

Donde la ley lo pida, avisa a las personas de que un agente les escribe. La política de uso aceptable tiene una línea sobre esto. Leer la política de uso aceptable

Una respuesta se rechaza por uno de estos motivos. Cuando una fila tiene dos nombres, el segundo es el final de messageKey, que empieza con serverErrors.

409 CONFLICTinboxRepliesOnly
La llamada abriría una conversación, haría un seguimiento de un mensaje tuyo o iría a una dirección distinta de la de quien envió. Una bandeja de entrada de agente solo responde. Para escribir primero, envía desde un buzón que conectaste.
409 CONFLICTinboxReplyAutomated
Una máquina escribió el mensaje, así que nadie lo responde. Dos máquinas que se responden entre sí no paran nunca. details.messageId trae el ID del mensaje.
409 CONFLICTinboxSenderUnauthenticated
Nada prueba que el mensaje viniera del dominio de quien lo envió. Cualquiera puede escribir cualquier dirección en el campo De, y la respuesta iría allí. Para responder a esa persona de todos modos, envía desde un buzón que conectaste. details.messageId trae el ID del mensaje.
409 CONFLICTmailboxUnderReview
Nuestro equipo puso la bandeja en espera mientras revisa una denuncia sobre ella, así que no sale nada de ella. details.ground dice el motivo. Cita details.caseNumber cuando nos escribas.
409 CONFLICTmailboxAutomaticHold
Demasiados correos del dominio de la bandeja se marcaron como spam, rebotaron o se rechazaron como spam, así que las respuestas de todas las bandejas de entrada de agente de ese dominio quedan en espera hasta que nuestro equipo las vuelva a activar. details.ground dice cuál fue: complaint_rate, bounce_rate, policy_bounce_rate o policy_abuse_burst. Cita details.caseNumber cuando nos escribas.
409 CONFLICTinboxFirstContactReview
Nuestro equipo está revisando denuncias sobre la bandeja, así que solo responde a quienes tu workspace ya escribió antes, y quien envió este mensaje es un contacto nuevo. Cita details.caseNumber cuando nos escribas.
429 QUOTA_EXCEEDEDinboxThreadDay
La bandeja ya envió 10 respuestas en esta conversación en las últimas 24 horas. details.resetAt dice cuándo se libera un lugar, y la cabecera Retry-After dice cuántos segundos esperar. Una respuesta cancelada antes de salir devuelve su lugar.
429 QUOTA_EXCEEDEDquotaAgentInboxDay
La bandeja ya usó el límite de envíos de hoy. details.resetAt dice cuándo termina el día.
402 PLAN_GATEagentRepliesOff
Las respuestas a personas de fuera del workspace todavía no están abiertas para él. details.gate es agentReplies y details.upgradeTo es null: ningún plan las abre, así que escribe a [email protected]. Las respuestas a las otras bandejas de agente de tu workspace siguen saliendo.
429 QUOTA_EXCEEDEDquotaAgentRepliesDay
El workspace ya usó el límite de hoy para respuestas a personas de fuera. details.scope es agentRepliesDay, y details.resetAt dice cuándo termina el día.

Cómo se firman tus respuestas y adónde van los rebotes

Toda respuesta de una bandeja de entrada de agente se firma con la clave DKIM de tu propio dominio. Con la firma, el otro servidor de correo comprueba que el mensaje vino de tu dominio y que nadie lo cambió por el camino. No tienes que hacer nada. Nosotros creamos la clave y la cambiamos cada 365 días, y los registros que agregaste siguen igual.

Si los registros de un dominio fallan en nuestra comprobación durante 72 horas, dejamos de enviar desde él. El envío responde 409, con la clave de mensaje mailDomainNotSigning, y la bandeja sigue recibiendo correo. Corrige los registros en la página del dominio, en la consola, y pulsa Comprobar ahora.

Cuando un servidor de correo devuelve un mensaje para siempre (la dirección no existe, el buzón se cerró), el rebote llega a una dirección nuestra, hecha solo para ese correo. Lo unimos a tu envío, rellenamos bouncedAt y disparamos send.bounced una vez. Para verlo, lee el envío:

const send = await colding.sends.get({ sendId });
console.log(send.status, send.bouncedAt);
// sent 2026-10-14T09:31:07.000Z

El status sigue en sent, porque el mensaje salió. bouncedAt es la hora en que llegó el rebote. Solo cuenta el rebote que responde a un correo tuyo. Un mensaje que nombra el envío de otra persona, o uno inventado para parecer un rebote, se descarta. El rebote de una respuesta de agente todavía no entra en la tasa de rebote de tu workspace, la que puede restringir un workspace. Contará cuando el registro de nuestro servidor de correo pueda confirmarlo.

Una bandeja de entrada de agente puede responder a personas a las que el workspace nunca escribió: hasta 50 al día por bandeja y 200 al día en todo el workspace. El día termina a medianoche en la zona horaria del workspace. Quienes ya recibieron un correo del workspace no cuentan. Pasado el límite, el envío responde 429, con details.scope (agentFirstRepliesInbox o agentFirstRepliesWorkspace), limit y resetAt. Un envío que programaste se cancela cuando llega al límite.

Enviar un correo y seguirlo

Una clave para una bandeja de entrada de agente

Una clave con read lee todas las bandejas del workspace. Para mantener a un agente lejos del correo de los demás, un dueño puede crear una clave que lea solo la bandeja de ese agente. El dueño la crea en la consola, en Configuración › Claves de API, o pulsa Crear una clave para esta bandeja de entrada de agente en la fila de la bandeja. La clave recibe read y send. Una clave no puede crear claves, así que el agente se lo pide al dueño.

Esta clave lee su bandeja y nada más. Una conversación, un mensaje o una bandeja que no está en su lista responde 404, igual que respondería por algo que no existe. Una lista, como threads.list, trae solo el correo de su bandeja.

Cambia la clave y los IDs por los tuyos. Las primeras llamadas leen el mensaje más reciente de la bandeja. La última pide otra bandeja y recibe un 404:

const threads = await colding.threads.list({ mailboxId: myInbox });
const newest = threads.items[0];
if (newest) {
  const messages = await colding.threads.messages({ threadId: newest.id });
  const last = messages.items[0];
  if (last) {
    const detail = await colding.messages.get({ messageId: last.id });
    console.log(detail.message.subject, detail.body.text);
  }
}

try {
  await colding.agentInboxes.get({ inboxId: otherInbox });
} catch (error) {
  // 404, the same answer as for an inbox that does not exist.
  if (!(error instanceof ColdingError) || error.status !== 404) throw error;
  console.log("This key can't see that inbox.");
}

El resto del workspace queda cerrado para esta clave. Los contactos, las campañas, las alertas y las demás llamadas del workspace responden 403, con details.scope igual a keyReads. Tampoco puede abrir el flujo de eventos en vivo, y recibe el mismo 403.

events.list le muestra a esta clave solo los eventos de sus bandejas, los que tienen data.mailboxId en su lista. Un evento sin mailboxId queda fuera. Para recuperar lo que llegó durante una pausa, guarda el resumeCursor de la última página que procesaste y pásalo como cursor la próxima vez.

Una conversación puede tener correo de más de una bandeja, como un mensaje enviado a dos de tus direcciones. Esta clave ve solo sus copias. El número de mensajes, la lista de bandejas y la hora del último mensaje salen solo de esas copias. Marcar la conversación como leída y moverla cambian solo la copia de la clave. Las etiquetas y las posposiciones son de la conversación entera, así que en una conversación que también tiene correo de una bandeja que la clave no lee responden 403, con details.scope igual a keyReads.

Con hasta 20 agentes, da a cada uno su propia clave. Por encima de eso, tu backend recibe el webhook y enruta por mailboxId.

Cómo funcionan los eventos, los cursores y las restauraciones

Elimina una bandeja

agentInboxes.delete elimina una bandeja. No se puede deshacer.

const deleted = await colding.agentInboxes.delete({ inboxId });
console.log(deleted.id, deleted.heldUntil ?? "held for good");

Desde ese momento, nuestro servidor de correo rechaza el correo a la dirección con un error permanente (un 550), y el servidor de quien lo envió le avisa de que la dirección no existe. El correo que la bandeja guardaba se borra con ella, en menos de una hora. La bandeja deja de contar en las bandejas y el almacenamiento de tu plan.

La dirección queda reservada. heldUntil es el día en que otro workspace puede usarla, 90 días después de eliminarla. Sin esa reserva, correo que era para ti podría llegar a otra persona. Tú puedes crear la dirección de nuevo en cualquier momento. En un dominio nuestro, la dirección queda reservada para siempre y heldUntil es null.

Eliminar es la única forma de que deje de llegar correo a una dirección. Pausar solo detiene las respuestas.

Claves y correo no confiable

Todo lo que hay en un mensaje lo escribió quien lo envió: el asunto, el nombre de quien lo envió y el cuerpo. El agente lo trata como datos, nunca como instrucciones ni como aprobación para hacer algo. Leer un mensaje no da ningún permiso.

Una bandeja de entrada de agente solo responde al correo que llegó. Nunca escribe primero a alguien que no le escribió.

Una clave con read lee todas las bandejas del workspace, a menos que un dueño le haya dado una lista de bandejas para leer. Dale a cada agente solo la lista que necesita y no entregues ninguna clave a código que tú no ejecutas.