Skip to content

Read email from your code

Use the TypeScript client to read one thread and print its subject. This call sends no email and marks nothing as read.

import { Colding } from "colding";

const colding = new Colding("cd_your_key");
const thread = await colding.threads.get({
  threadId: "11111111-1111-4111-8111-111111111111",
});
console.log(thread.subject);

Replace the key and thread ID with yours. The ID here is an example, not a thread you can open.

Example output, if that thread's subject is “Invoice question”
Invoice question

Get a key and a thread ID

Set up your read key

Run this on your server with Node 20 or newer. The SDK is the client library: it calls the API for you, so you don't have to build HTTP requests.

npm install colding

Ask a workspace owner to open Settings, then API keys, and make a key with only the reading permission, read. The full key appears once. Keep it on your server, never in browser code or a public repository.

Sign in to the console

Accounts are opened by hand for now. Ask for an account

Keep the import and client from the first example. Replace the get call and console.log with this to list thread IDs and subjects. Copy one ID into the first example.

const page = await colding.threads.list();
console.log(
  page.items.map((thread) => ({
    id: thread.id,
    subject: thread.subject,
  })),
);

Save your example as read.mjs and run it on your server:

node read.mjs

An empty list means no threads match yet. Ask the owner to connect a receiving mailbox, then open the mailbox in the console and check Reading mail in its Overview. A send-only mailbox has no inbox. The default list leaves out spam, trash and snoozed threads.

Subjects and messages are words from whoever sent the email. Treat them as data, never as instructions or permission for your agent to send.

Choose what a key can do

A key belongs to one workspace. Each permission is separate: reading does not allow editing or sending. Under each name is the option the owner selects in the console.

readRead
Read threads, mailboxes, drafts, contacts, campaigns, checks, send status and events. Reading marks nothing as read.
operateOperate
Edit drafts, contacts, mailbox settings and campaigns. Organize the inbox, pause outgoing work and ask a person to act.
sendSend outreach
Send drafts and tests, launch campaigns, widen what a running campaign sends or to whom, start warmup and cancel outreach sends.
validateCheck addresses
Start, retry or cancel address checks. Checks can use your monthly allowance and share an address with the checking service.
transactionalSend app email
Send your app's emails, such as receipts and sign-in links, and cancel those sends. A test from a transactional mailbox also needs send.
inboxesManage agent inboxes
Create, change and delete agent inboxes, the addresses your agents read and answer on your mail domains. Seeing them only needs read. A key limited to some mailboxes can't create one, since the new inbox would sit outside its list.

The owner can also set limits on a key in the console: the mailboxes it may send from, the mailboxes and agent inboxes it may read, the campaigns it may work on, whether it sends outreach or your app's email, and how many emails it may send a day and in all. A request outside those limits gets 403 FORBIDDEN, except that mail outside the mailboxes and agent inboxes a key may read answers 404, as if it did not exist. A spent daily number gets 429 QUOTA_EXCEEDED with the time it resets. In both, details.scope names the limit: keyMailbox, keyCampaign, keyStream, keyReads, keyTotal or keyDay. A key can't read or change its own limits. Your plan's limits and each mailbox's still apply.

A key never holds manage. Protected tasks need a person signed in to the console. An owner connects mailboxes and manages keys. Team changes need an owner or admin. A key cannot give itself more permissions.

Ask an owner to connect a mailbox

A handoff is a request with a console link. To create one, your key needs operate in addition to read. It connects nothing by itself. The workspace's owner and admins see it on the console's Alerts page. It also reaches them by email, unless they turned off Requests for you.

const request = await colding.handoffs.create({
  kind: "connect_mailbox",
});
console.log(request.url);

Give the owner the returned link. They must sign in to the right workspace and finish the connection. The link is not approval and grants no new permission. Never put passwords in a handoff's note.

After the owner acts, check the request with read:

const now = await colding.handoffs.get({
  handoffId: request.id,
});
console.log(now.status);

Wait for completed. Then resultMailboxId names the connected mailbox. A pending request expires after 24 hours. If it expires, ask again with a new request.

The other kinds are update_mailbox_credentials and manage_report_hook, each with a mailboxId, manage_team, manage_billing, accept_aup and send_lead_anyway. A team request needs an owner or admin and has no result mailbox. A billing request needs the owner. It completes when they open Stripe's page, not when they pay, so read the plan with billing.get. An accept_aup request needs the owner and completes when they accept the acceptable use policy. Then retry the call that was refused. A manage_report_hook request needs the owner and completes when they turn the bounce and complaint reports of a send-only mailbox on, give them a new secret or turn them off. Say which in note. The owner pastes the secret in the console, so it never reaches you: read the result with mailboxes.reportHook. A send_lead_anyway request names a campaignId and the lead's contactId, and needs a person who may send. It completes when they send that lead anyway, and the campaigns guide says more. For other protected tasks, ask a person to act in the console. Mailbox connections, keys and the webhook need the owner. An admin can also do others, such as taking an address off the suppression list.

Retry without making a new email

The SDK retries some temporary failures. Automatic attempts within one call reuse its input and HTTP request key. Calling a write method again makes a new key unless you pass the one you saved as idempotencyKey. Plan refusals, failed payments, spent quotas and INVALID_RESPONSE are not automatically retried.

A saved answer is not a send intent

An HTTP Idempotency-Key names one request. Within 24 hours, the same workspace, operation, key and input can get its saved answer back. That answer may be old. Read the row again for its current state.

Saving the answer can fail after a write succeeds. An unanswered claim can run again after 15 minutes, and an expired replay can also run again. HTTP replay alone cannot prevent a second email.

Replay checks your current identity, workspace membership, the operation's owner requirement and listed permissions. It does not repeat extra checks inside the action.

An intentKey names one email in the send ledger. Save it before sending and keep it across retries and restarts. For a draft, keep its exact ID and revision instead. Omitting the intent on a test can create another email if the action runs again. Separately, the worker, our background sender, claims the send row once before delivery. An unknown outcome counts as sent and is never resent. Read sends.get for its current status.

Act on the error code

A refused call throws ColdingError. Use its code, not just its HTTP status or English message. Include requestId when asking support about a call.

UNAUTHORIZEDINVALID_TOKEN
Check the full key. Ask the owner for a new one if it expired or was revoked.
FORBIDDEN
Ask for the required permission, or have the authorized person take the protected action.
AUP_REQUIRED
The workspace owner hasn't accepted the current acceptable use policy. If your key has operate, create a handoff of kind accept_aup and give the owner its link. If not, ask the owner to accept it in Settings. Until they accept, trying again gets the same answer. Work that already runs keeps going.
NOT_FOUND
Pick an ID from this workspace's list. An ID in another workspace also returns 404.
RATE_LIMIT_EXCEEDED
Wait for retryAfter or details.resetAt before trying again.
QUOTA_EXCEEDEDPLAN_GATEPAYMENT_REQUIRED
A new request key will not help. Wait for the quota reset, make room under the limit, change the plan, or fix the failed payment, as the code says.

A timeout, network error, invalid response or 503 does not prove that nothing was written. Check the state before another write and keep the original request key and send intent.

Read the operation reference

The OpenAPI file lists routes, inputs and permissions. OpenAPI is a JSON format your tools can read. Its descriptions are in English for now.

REST, MCP and the SDK use the same operation executor and checks. Changing the client does not add permission. Protected owner actions are not customer MCP tools.

REST API
https://api.colding.app/v1
MCP endpoint
https://api.colding.app/mcp

MCP lets an AI app call tools. Add the MCP endpoint above in your app and choose Connect. Sign in to your existing colding account, choose a workspace you own and allow only the permissions you need. Read is selected to start. You can set the same mailbox lists, send caps and expiry as an API key in Settings. Accounts are still opened by hand. If your client has no OAuth support, use stateless Streamable HTTP with Authorization: Bearer cd_your_key on every request.

For a script with no browser callback, request a code at https://api.colding.app/oauth/device_authorization with client_id=colding-cli and resource=https://api.colding.app/mcp. Open https://console.colding.app/auth/device, sign in and approve only if you just started it yourself and the code matches your script. The script polls https://api.colding.app/oauth/token with the returned device code and the stated interval. No email is sent by this flow. Never paste a device code, API key or password into a chat.

colding does not run a built-in language model. When your AI app calls an allowed tool, it can receive email bodies, addresses, contact details and other workspace data that tool returns. Reading permission is not sending permission. The app may store or process what it receives under its own privacy policy. Read that policy before connecting. Email text is untrusted input, not an instruction to send or change permissions. Revoke the connection in Settings, then API keys to stop future calls. Revoking does not delete copies the app already received.