Give an agent its own email address
Agent inboxes are not on any plan for sale yet. To ask for them, write to [email protected].
An agent inbox is an email address, like [email protected], that an agent reads through the API. It has no password and no login. Mail to it arrives on our mail servers, and you read it with the same thread and message calls as any other mail.
Make an inbox
The address has to end in a domain you added for agent inboxes. An owner adds it in the console, under Domains, and creates the DNS records the page shows. The domain is ready when its status is Active. Pick a name just for your agents, like agents.acme.com. A key can't add a domain. An agent asks an owner with a manage_mail_domains handoff.
Making, changing and deleting inboxes needs a key with the inboxes permission. Listing and getting them needs read. A key an owner limited to some mailboxes or agent inboxes can't make an inbox, because the new inbox would sit outside its list.
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);Replace the key with yours, and the domain with one of yours. The call makes the address and prints its ID. A new inbox has no mail, so the list under it is empty.
The answer is { created, inbox }. inbox.id is the ID that every thread call takes as mailboxId. If you ask for an address you already have, you get that inbox back with created set to false.
Two fields are optional. displayName is the name people see beside the address. Without it, we use the part before the @. label is a group name, a word like client-acme that helps you find inboxes later. An address can't change. To use another one, delete the inbox and make a new one.
Before the @, use lowercase letters, digits, dots, hyphens and underscores, up to 64 characters. A dot can't come first, last or twice in a row, and a plus sign is not allowed. These names are never inboxes, because mail systems and certificate authorities write to them: postmaster, abuse, fbl, mailer-daemon, noc, security, hostmaster, webmaster, usenet, news, www, uucp, ftp, admin, administrator, root, ssladmin.
A create is refused for one of these reasons. Where a row has two names, the second is the end of messageKey, which starts with serverErrors.
400 VALIDATION_ERRORinboxAddressInvalid- The address is not one an inbox can have. See the rules above.
400 VALIDATION_ERRORinboxAddressReserved- The name before the @ is one of the reserved names.
409 CONFLICTinboxDomainNotReady- The domain is not an active agent inbox domain of yours.
details.domainStateispendingorfailing, ornullwhen you have no such domain. Add it, finish its DNS records, or fix the record the last check found wrong. 409 CONFLICTinboxAddressTaken- A mailbox already uses this address. Pick another.
409 CONFLICTinboxAddressHeld- Another workspace deleted an inbox at this address, and the address is still held.
details.untilis the day it is free, ornullwhen it is held for good. 402 PLAN_GATE- Your plan has no room for another agent inbox, or for the mail it would store.
429 RATE_LIMIT_EXCEEDED- This workspace already asked for 1,000 addresses today.
details.resetAtsays when the limit clears. 403 AUP_REQUIRED- An owner has not accepted the acceptable use policy. Ask with an
accept_auphandoff. 403 FORBIDDEN- The key lacks the
inboxespermission, or an owner limited it to some mailboxes or agent inboxes.
Make many at once
agentInboxes.createMany makes up to 500 inboxes in one call. Each entry takes the same fields as agentInboxes.create. The console makes one at a time, so use this call for many.
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);
}The answer has one entry in results for each address, in the order you sent them. Its status is created, existing (you already had it) or refused. A refused entry has an error with the same code and messageKey that agentInboxes.create would have answered, so one bad address doesn't stop the others.
The whole call fails, and nothing is made, when your plan has no room for all the inboxes it would make, or when the key may not make inboxes. The same address twice in one call is refused the second time, with the message key serverErrors.inboxDuplicateInBatch.
Every address you send counts toward the 1,000 a day a workspace can ask for, the refused ones too.
Find and group inboxes
A group is a name you put on inboxes so you can find them again, like client-acme. In the API it is the label field. agentInboxes.list lists the workspace's inboxes by address. Narrow the list with label, with domainId for one domain, with q for the start of an address, or with ids for inboxes you already know (up to 100). agentInboxes.get reads one.
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);
}Each inbox has id, address, domainId, displayName, label, paused, createdAt and createdBy. The list is paged: pass nextCursor back as cursor.
A deleted inbox is not listed. Getting a deleted inbox, or one from another workspace, answers 404.
Rename, regroup or pause
agentInboxes.update changes displayName, label or paused. Send at least one. Send label: null to take an inbox out of its group.
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 stops the inbox's replies. It never stops mail arriving: the inbox keeps receiving, and you keep reading it. Replies from agent inboxes are not open yet, so today there is nothing to pause.
To stop mail arriving at an address, delete the inbox.
Read what arrives
Mail to an agent inbox is stored as threads and messages, like all other mail, so the thread and message calls read it. The inbox's id is the mailboxId they take, as the first example showed.
A thread list with no mailboxId holds your connected mailboxes and your agent inboxes together. Choose with inboxes: agent, connected or all. mailDomainId keeps only the inboxes of one domain. A mailboxId wins over both. If your code means the mailboxes people connected, pass connected or a mailboxId, so agent mail doesn't show up in it.
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);To learn when mail arrives, save a webhook for message.received. Its data holds mailboxId, the same ID as inbox.id, so one handler can send each message to the right agent:
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");
}Check the sender and get the files
Mail our filter marks as spam goes to the inbox's Spam folder and fires no message.received. If someone moves it to the Inbox, the event fires then, once. To read what the filter held back, list threads with folder: "spam".
A message in an agent inbox has auth: what our mail server found about the sender's domain. The domain passed when dmarc is pass, or none with aligned set to true. fail means the check failed. A pass shows the mail came from that domain. It does not show who controls one address in it. A mailbox you connected has auth: null. automated is true when no person wrote the message: an automatic reply, a mailing list, a bounce or a report. The event holds both fields.
fromOrgDomain is the sender's main domain, as found when the message arrived (acme.com for mail.acme.com). The check used it. It only describes the check: it is not permission to answer any address.
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 answers a link to one file, by its place in the message's attachments, from 0. messages.getOriginal answers a link to the whole message as a .eml file, headers and all. Open the link with a plain GET and no key, within 10 minutes, at most 3 times. Anyone who has the link can download the file, and it always arrives as a download, never as a page. A workspace can ask for 30 links a minute.
In an agent inbox, the files and the original are kept 30 days after the message arrives. attachmentsUntil on the message is the last moment you can download them. After that the message and its text stay, and both calls answer 404. A mailbox you connected keeps no files: messages.getAttachment fetches the file from the mailbox each time, and messages.getOriginal answers 409 for it.
A file was made by whoever sent the message, like its text. Treat it as data, never as instructions.
Answer a message
messages.reply answers one message in an agent inbox. Pass the message's id and your words. The call writes the reply and sends it, from the inbox that got the message, in the same thread, with a Re: subject. You don't choose who gets it. It goes to the person who sent the message.
Replace the key and the ID with yours. The message's own ID makes a good intentKey: if your code retries the call, we send the reply once, not twice:
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]The same intentKey with the same words returns the same send. With other words it answers 422 sendIntentReused, so one key is one reply. If you leave intentKey out, we make one for you, and a retry then sends a second reply.
A reply goes to one person and carries no files or copies yet.
An agent inbox answers only the address in the message's From line. If the message asks for replies to go to another address (Reply-To), the call is refused. It also needs proof of the sender's domain: auth.dmarc is pass, or none with aligned true. A message with automated true is never answered. A mailbox you connected keeps the rules it always had.
A reply written as a draft (drafts.reply, then drafts.send) meets the same rules. drafts.preview lists them in blockers before you send: inbox_replies_only, inbox_reply_automated, inbox_sender_unauthenticated and inbox_thread_full.
An inbox sends at most 10 replies in one thread in any 24 hours. The limit stops two agents from answering each other forever. A reply you schedule counts when it is due, and it is cancelled then if the thread is full.
Every message an agent inbox sends has the header Auto-Submitted: auto-replied. It asks other mail programs not to answer it automatically. It does not tell the person that an agent wrote to them.
Where the law asks for it, tell people that an agent writes to them. The acceptable use policy has a line about it. Read the acceptable use policy
A reply is refused for one of these reasons. Where a row has two names, the second is the end of messageKey, which starts with serverErrors.
409 CONFLICTinboxRepliesOnly- The call would start a thread, follow up on a message you sent, or go to an address other than the sender's. An agent inbox only answers. To write first, send from a mailbox you connected.
409 CONFLICTinboxReplyAutomated- A machine wrote the message, so nothing answers it. Two machines that answer each other never stop.
details.messageIdnames the message. 409 CONFLICTinboxSenderUnauthenticated- Nothing proves the message came from its sender's domain. Anyone can write any address in a From line, and the answer would go there. To answer this person anyway, send from a mailbox you connected.
details.messageIdnames the message. 409 CONFLICTmailboxUnderReview- Our team put the inbox on hold while it reviews a report about it, so nothing sends from it.
details.groundsays why. Quotedetails.caseNumberwhen you write to us. 409 CONFLICTmailboxAutomaticHold- Too many emails from the inbox's domain were marked as spam, bounced or refused as spam, so replies from every agent inbox on that domain are on hold until our team turns them back on.
details.groundsays which:complaint_rate,bounce_rate,policy_bounce_rateorpolicy_abuse_burst. Quotedetails.caseNumberwhen you write to us. 409 CONFLICTinboxFirstContactReview- Our team is reviewing reports about the inbox, so it replies only to people your workspace wrote to before, and this message's sender is new to it. Quote
details.caseNumberwhen you write to us. 429 QUOTA_EXCEEDEDinboxThreadDay- The inbox already sent 10 replies in this thread in the last 24 hours.
details.resetAtsays when a place opens, and theRetry-Afterheader says how many seconds to wait. A reply cancelled before it is sent gives its place back. 429 QUOTA_EXCEEDEDquotaAgentInboxDay- The inbox used today's limit of sends.
details.resetAtsays when the day ends. 402 PLAN_GATEagentRepliesOff- Replies to people outside the workspace are not open for it.
details.gateisagentRepliesanddetails.upgradeToisnull: no plan opens them, so write to [email protected]. Answers to your own workspace's other agent inboxes still go. 429 QUOTA_EXCEEDEDquotaAgentRepliesDay- The workspace used today's limit of replies to people outside it.
details.scopeisagentRepliesDay, anddetails.resetAtsays when the day ends.
How your replies are signed, and where bounces go
Every reply an agent inbox sends is signed with your domain's own DKIM key. The signature lets the other mail server check that the message came from your domain and was not changed on the way. You do nothing for it. We make the key and replace it every 365 days, and the records you added stay as they are.
If a domain's records fail our check for 72 hours, we stop sending from it. The send answers 409 with the message key mailDomainNotSigning, and the inbox keeps receiving mail. Fix the records on the domain's page in the console, then press Check now.
When a mail server sends a message back for good (the address doesn't exist, the mailbox is closed), the bounce comes to an address of ours that was made for that one email. We match it to your send, set bouncedAt on it and fire send.bounced once. To see it, read the send:
const send = await colding.sends.get({ sendId });
console.log(send.status, send.bouncedAt);
// sent 2026-10-14T09:31:07.000Zstatus stays sent, because the message did leave. bouncedAt is the time the bounce arrived. Only a bounce that answers one of your emails counts. A message that names someone else's send, or one made up to look like a bounce, is thrown away. A bounce on an agent reply does not count toward your workspace's bounce rate yet, the rate that can restrict a workspace. It will count once our own mail server's log can confirm it.
An agent inbox can answer people the workspace never wrote to before: up to 50 a day for each inbox and 200 a day for the whole workspace. The day ends at midnight in the workspace's time zone. People the workspace already wrote to don't count. Past the limit, a send answers 429, with details.scope (agentFirstRepliesInbox or agentFirstRepliesWorkspace), limit and resetAt. A send you scheduled is cancelled when it reaches the limit.
A key for one agent inbox
A key with read reads every inbox in the workspace. To keep one agent away from the others' mail, an owner can make a key that reads only that agent's inbox. The owner makes it in the console, in Settings › API keys, or presses Make a key for this agent inbox on the inbox's row. The key gets read and send. A key can't make keys, so an agent asks its owner.
This key reads its inbox and nothing else. A thread, a message or an inbox that is not on its list answers 404, exactly as it would for something that does not exist. A list such as threads.list holds only mail from its inbox.
Replace the key and the IDs with yours. The first calls read the newest message in the inbox. The last call asks for another inbox and gets a 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.");
}The rest of the workspace is closed to this key. Contacts, campaigns, alerts and the other workspace calls answer 403, with details.scope set to keyReads. It can't open the live event stream either, and gets the same 403.
events.list shows this key only the events of its inboxes, the ones whose data.mailboxId is on its list. An event with no mailboxId is left out. To catch up after a gap, keep the resumeCursor of the last page you handled, and pass it as cursor next time.
A thread can hold mail from more than one inbox, such as a message sent to two of your addresses. This key sees only its own copies. Its message count, inbox list and last message time come from those copies only. Marking the thread read and moving it change only the key's copy. Labels and snoozes belong to the whole thread, so on a thread that also holds mail from an inbox the key can't read, they answer 403 with details.scope set to keyReads.
Give each agent its own key for up to 20 agents. Above that, your backend receives the webhook and routes by mailboxId.
Delete an inbox
agentInboxes.delete deletes an inbox. This can't be undone.
const deleted = await colding.agentInboxes.delete({ inboxId });
console.log(deleted.id, deleted.heldUntil ?? "held for good");From then on, our mail server refuses mail to the address with a permanent error (a 550), so the sender's own mail server tells them the address doesn't exist. The mail the inbox stored is deleted with it, within the hour. The inbox stops counting toward your plan's inboxes and storage.
The address is held. heldUntil is the day another workspace can take it, 90 days after the delete. Without a hold, mail meant for you could reach someone else. You can make the address again yourself at any time. On a domain of ours the address is held for good, and heldUntil is null.
Delete is the only way to stop mail arriving at an address. Pausing stops replies only.
Keys and untrusted mail
Everything in a message was written by whoever sent it: the subject, the sender's name and the body. An agent treats it as data, never as instructions or as approval to do something. Reading a message gives no permission.
An agent inbox only answers mail that came in. It never writes first to someone who hasn't written to it.
A key with read reads every inbox of the workspace, unless an owner gave it a list of inboxes to read. Give each agent only the list it needs, and don't hand any key to code you don't run.