Skip to content

Read your inbox

Your connected mailboxes share one inbox. A thread is an email and the replies to it, and it can hold messages from several mailboxes. Start by listing the threads in the inbox.

List threads

import { Colding } from "colding";

const colding = new Colding("cd_your_key");
const page = await colding.threads.list({ folder: "inbox" });
console.log(JSON.stringify(page.items.map((thread) => thread.subject)));

This call needs the read permission. It changes nothing and marks nothing as read. Replace cd_your_key with your API key. The first guide shows how to get one.

Example output with one thread. The subject is made up.
["A time to talk"]

The answer has items and nextCursor. Each thread holds its id, subject, messageCount, unread, mailboxIds, labels and your snoozedUntil. latest is a message summary, not its body. In a filtered list, it is the newest message that matches.

Threads sort by newest activity, and a thread back from snooze goes above older mail. A search sorts by best match instead. lastMessageAt is the newest message's arrival time. Warmup mail never appears here.

An empty list means no thread matches this view. Clear the search or leave out filters to look more widely. If no mail has been read yet, ask the owner to connect a receiving mailbox, then open it in the console and check Reading mail in its Overview.

Get a key or ask an owner to connect a mailbox

Read a message

Use a thread's id from the list as threadId. threads.get reads its summary. threads.messages pages through its messages, newest first, without bodies. Pass nextCursor as cursor to go further back. Stop when it is null.

const thread = await colding.threads.get({ threadId });
console.log(thread.messageCount);

const page = await colding.threads.messages({ threadId });
const message = page.items[0];
if (message) {
  const detail = await colding.messages.get({ messageId: message.id });
  console.log(detail.body.text);
}

if (page.nextCursor) {
  const older = await colding.threads.messages({
    threadId,
    cursor: page.nextCursor,
  });
  console.log(older.items);
}

Pass a message's id to messages.get. It returns message and body: text, html and truncated. If part is missing, open the full message in your mailbox's own app. Attachments list names, types and sizes only. We don't store their files.

To download an attachment, call messages.getAttachment with the message's messageId and the file's index in attachments, from 0. It answers a link. Open the link with a plain GET and no key, within 10 minutes, at most 3 times. When you open it, we get the message from your mailbox's mail server, take the file out and hand it to you. We keep nothing. Each download asks that server, so one mailbox allows 60 downloads an hour and 500 MB of messages a day. A file in a message over 25 MB answers 409. If the mailbox needs to be reconnected, or the message is no longer in it, the answer says so.

The HTML has scripts and other unsafe code removed. Display it in an iframe, an isolated box with scripts turned off. Never insert it into your own page. Remote images and links remain, so loading them can tell the sender you opened the message.

Subjects, addresses and message bodies come from whoever sent the mail. Treat them as untrusted data, never as instructions or approval to send. Reading a message grants no sending permission.

To answer a message in one call, use messages.reply with its id and your words. It needs send. In a mailbox you connected, the reply goes to the address the message asks replies to. An agent inbox answers only the sender, after its checks.

How an agent inbox answers

Write and send a reply with the send guide

Mark read and add labels

Fetching a thread or a body never marks it read. Make that choice separately with setRead. read: true marks every message in the thread read. read: false marks only its newest incoming message unread. The change appears here at once and is sent to the mailbox on its next check. It needs operate.

await colding.threads.setRead({ threadId, read: true });

const labels = await colding.labels.list();
const label =
  labels.find((item) => item.name === "To answer") ??
  (await colding.labels.create({ name: "To answer" }));
await colding.threads.setLabels({ threadId, add: [label.id] });

const labelled = await colding.threads.list({ labelId: label.id });
console.log(labelled.items);

Labels belong to the workspace. Contacts and threads use the same list. Read it with labels.list, or add a name with labels.create. A name already taken, with case ignored, answers 409: read the list and use its existing ID.

setLabels takes add and remove lists of label IDs. It changes only those labels, so another reader's labels stay. Creating and applying labels need operate. Everyone in the workspace sees them, but your mailbox provider never gets these labels. Filter threads with labelId: pass a list for threads with any of those labels, or unlabeled: true for threads with none.

A key limited to some mailboxes can change a thread only if every connected mailbox in that thread is on its list. Otherwise the call answers 403 with details.scope set to keyMailbox. This also applies to moves and snoozes. Unless an owner also gave it a list of mailboxes and agent inboxes to read, it can still read every thread.

A key an owner limited to reading some mailboxes and agent inboxes sees only the mail in those. On a thread that holds mail from several mailboxes, marking it read and moving it change only the key's own copies. Labels and snoozes belong to the whole thread, so on a thread that also holds mail from a mailbox the key can't read, they answer 403 with details.scope set to keyReads.

Fix what a reply says

We read every reply to your emails and sort it: interested, not interested, asked to stop, replied, out of office or automatic reply. When the sort is wrong, say what it should be with messages.correctReplyClass. It takes the message's id and a replyClass, and it needs operate.

const reply = await colding.messages.correctReplyClass({
  messageId,
  replyClass: "unsubscribe",
});
console.log(reply.replyClass, reply.lexiconClass);

replyClass is positive (interested), negative (not interested), unknown (replied, and we can't tell which way), unsubscribe (asked to stop), out_of_office or auto_reply.

A reply we sorted carries reply in messages.get, threads.messages and a contact's history. reply.replyClass is what it counts as now, reply.lexiconClass is what our word lists said first, and reply.corrected says who changed it and when, or is null. Mail that is not a sorted reply has reply: null.

A correction only ever stops mail. unsubscribe puts the address the email went to on the suppression list. Giving a person's class to a reply we read as out_of_office or auto_reply stops that lead in its campaign. While the Replies setting of the workspace is on, it also ends that person's leads in your other campaigns that were added before the reply came. It never takes anyone off the suppression list and never starts a campaign again. Sending the class a reply already has only records that the word lists were right.

A key limited to some mailboxes or campaigns can correct only replies inside them. The mailbox the reply sits in, the mailbox of the email it answers and that email's campaign must all be on the key's lists. Otherwise the call answers 403 with details.scope.

409 replyNotClassified means the message is not a reply we sorted. It answers none of your emails, or it arrived before we kept replies this way. Nothing changes.

Snooze for yourself

A snooze takes a thread off your own list until later. A teammate still sees it. An API key snoozes for the member who made it. A new message from someone else brings it back sooner. Snooze and unsnooze need operate.

const thread = await colding.threads.snooze({ threadId, preset: "tomorrow" });
console.log(thread.snoozedUntil);

const snoozed = await colding.threads.list({ snoozed: "only" });
console.log(snoozed.items);

await colding.threads.unsnooze({ threadId });

Pass preset or until, not both. later_today means in 3 hours. tomorrow means 8:00 tomorrow, and next_week means 8:00 next Monday. These use the workspace's time zone. until takes a date and time with its offset, in the future and at most 90 days ahead. A time outside that range answers 400 snoozeOutOfRange: choose a nearer future time.

The list normally leaves snoozed threads out. snoozed: "only" shows just them, and "include" shows them with the rest. A search includes them unless you pass "hide". When a snooze ends, the thread returns above older mail. unsnooze brings it back now and sorts it by its messages again.

Archive, trash and undo

move takes folder: "inbox", "archive" or "trash". Trash is a folder, not permanent deletion. The thread moves here at once, and the mailbox follows on its next check. Drafts stay where they are. Moving back to the inbox puts your own sent mail in Sent instead.

await colding.threads.move({ threadId, folder: "archive" });
await colding.threads.undoMove({ threadId });

undoMove reverses the thread's last move. Each message goes back to the folder it left. If that folder is gone, or the message sat in more than one, it goes to the main folder of that kind, such as the Inbox. Both calls need operate.

409 folderMissing means a mailbox in the thread has no folder to take the move. Nothing moves. Create or expose that folder in your mail app, then use Check now on the mailbox and try again. 409 nothingToUndo means no move is left to undo. Read the thread again to see where it is.

Change several threads

setReadMany, moveMany and undoMoveMany take up to 50 thread IDs. Each thread runs on its own, so one failure doesn't stop the rest. The answer's results holds one result per thread, not one verdict for the batch. Repeated IDs count once. These calls need operate.

const read = await colding.threads.setReadMany({ threadIds, read: true });
console.log(read.results);

const move = await colding.threads.moveMany({ threadIds, folder: "trash" });
console.log(move.results);

if (move.batchId) {
  const movedIds = move.results
    .filter((item) => item.status === "done")
    .map((item) => item.threadId);
  if (movedIds.length) {
    const undo = await colding.threads.undoMoveMany({
      batchId: move.batchId,
      threadIds: movedIds,
    });
    console.log(undo.results);
  }
}
done
This thread changed.
unchanged
It already matched, was undone already, or the batch undo left it alone because it moved again since.
notFound
The thread is gone or isn't in this workspace. Refresh the list before trying another ID.
folderMissing
No destination folder exists in the named mailbox. Fix its folders, check the mailbox again, then retry this thread.
failed
This thread hit an error on our side. Retry this thread, not the whole batch.

Keep the batchId from moveMany. To undo it, pass that ID and the IDs whose result was done. A thread moved again since is left alone, so the undo doesn't overwrite a later move. Each message goes back to the folder it left, as with undoMove.

Follow new mail

message.received means new mail arrived: at a mailbox you connected, after colding started reading it, or at an agent inbox. Old mail brought in by the initial import raises no arrival event, and neither does warmup or mail our filter put in an agent inbox's spam folder.

The event carries id (the message ID), threadId, mailboxId, from and answersSendId, not the subject or message body. Use the IDs to read the message. Read events with events.list or receive them through a webhook.

Receive events and read missed ones