Docs · SDKs

TypeScript SDK

A zero-dependency typed client for Node 20+.

View as Markdown

shell
npm install agentboxd

The TypeScript client is the npm package agentboxd. It has no dependencies: it uses the global fetch and node:crypto, and needs Node 20 or newer. It ships ES module and CommonJS builds with type declarations. With no options, the client reads AGENTBOXD_API_KEY and AGENTBOXD_BASE_URL (default https://api.agentboxd.com) from the environment.

client.ts
import { Agentboxd } from 'agentboxd';

const mr = new Agentboxd(); // or new Agentboxd({ apiKey: 'mr_...', baseUrl: 'https://api.agentboxd.com' })

#Methods

CallEndpoint
mr.inboxes.create({ username?, display_name?, client_id? })POST /v1/inboxes
mr.inboxes.list() · get(id) · delete(id)/v1/inboxes
mr.inboxes.createTemporary({ ttlSeconds })POST /v1/inboxes with ttl_seconds
mr.inboxes.pause(id, { reason? }) · resume(id)POST /v1/inboxes/:id/pause · /resume (the kill switch)
mr.emergencyStop({ reason? })POST /v1/emergency-stop (emergency stop of the whole workspace)
mr.escalation.get() · update({ contacts?, triggers?, … })GET · PUT /v1/escalation (human on call)
mr.escalation.inbox.get(id) · mr.escalation.inbox.update(id, { override?, contacts?, triggers? })GET · PUT /v1/inboxes/:id/escalation
mr.domains.create/list/get/verify/update/delete/v1/domains
mr.domains.rotateDkim(id) · activateDkim(id)POST /v1/domains/:id/dkim/rotate · /activate
mr.domains.zoneFile(id)GET /v1/domains/:id/zone-file
mr.messages.send(inboxId, { to, subject, text?, html?, attachments?, data?, type? }, { idempotencyKey? })POST …/messages/send (data/type: agent messaging; send<T> types data)
mr.messages.reply(inboxId, messageId, { text?, html?, reply_all?, data?, type? })POST …/reply
mr.messages.forward(inboxId, messageId, { to, cc?, bcc?, text?, include_attachments? })POST …/forward
mr.messages.batchGet(ids) · batchUpdate(ids, { add_labels?, remove_labels?, is_read? })POST /v1/messages/batch-get · /batch-update
mr.messages.list(inboxId, { labels?, is_read?, direction?, channel?, type? })GET …/messages
mr.messages.wait(inboxId, { timeout?, since?, from?, subject?, channel?, type? })GET …/messages/wait
mr.messages.waitForVerification(inboxId, { timeout?, since?, from?, include_unauthenticated? })GET …/verification
mr.messages.get(id) · update(id, { add_labels?, remove_labels?, is_read? })/v1/messages/:id
mr.messages.claim(inboxId, { limit?, lease_seconds?, consumer?, wait?, enriched?, analysis?, since? })POST …/messages/claim (claim/ack queue)
mr.messages.ack(id, leaseId, { mark_read? }) · nack(id, leaseId, { delay_seconds? }) · extend(id, leaseId, { lease_seconds? })POST /v1/messages/:id/ack · /nack · /extend
mr.inboxes.consume(inboxId, handler, { concurrency?, leaseSeconds?, signal?, onError? })The consumer loop: claims, extends the lease while handler(message, ctx) runs, acks when it returns, nacks with backoff when it throws. Runs until signal aborts
mr.threads.list(inboxId, filters?) · listAll({ inbox_id?, labels?, before?, after?, participant?, subject?, ascending? }) · get(id) · delete(id)/v1/threads · /v1/inboxes/:id/threads
mr.search(q, { inbox_id? })GET /v1/search
mr.inboxes.update(id, { display_name?, metadata? })PATCH /v1/inboxes/:id
mr.threads.update(id, { metadata?, add_labels?, remove_labels? })PATCH /v1/threads/:id
mr.contacts.list({ q?, label?, metadata? }) · get(id) · byAddress(address)/v1/contacts
mr.contacts.update(id, { name?, notes?, metadata?, add_labels?, remove_labels? })PATCH /v1/contacts/:id
mr.contacts.delete(id)DELETE /v1/contacts/:id
mr.knowledge.list/create/get/update/delete/v1/knowledge
mr.knowledge.search(q, { inbox_id?, limit? })GET /v1/knowledge/search
mr.messages.draftReply(id, { instructions?, save? })POST /v1/messages/:id/draft-reply
mr.messages.attachmentText(messageId, attachmentId, { offset?, maxChars? })GET /v1/messages/:id/attachments/:attachmentId/text (Document extraction)
mr.messages.extractAttachment(messageId, attachmentId, { schema, instructions? })POST /v1/messages/:id/attachments/:attachmentId/extract
mr.drafts.create(inboxId, { to?, subject?, text?, reply_to_message_id?, send_at? }) · list · listAll (org-wide) · get · update · delete/v1/inboxes/:id/drafts (Drafts)
mr.drafts.send(inboxId, draftId) · schedule(inboxId, draftId, sendAt) · cancel(inboxId, draftId)POST …/drafts/:draftId/send · /schedule · /cancel
mr.webhooks.create/list/get/update/delete/test/v1/webhooks
mr.deliverability()GET /v1/deliverability
mr.stream({ inboxIds?, eventTypes?, payload?, since? })GET /v1/stream (WebSocket; async iterator, reconnects and resumes)
mr.streamToken()POST /v1/stream/token
mr.identity.token({ inboxId | identityId, audience, nonce?, scope?, expiresIn? })POST /v1/inboxes/:id/identity-token (Sign in with Agentboxd)
mr.identities.create/list/get/update/delete · pause(id) · resume(id)/v1/identities (identities without a mailbox)
mr.identity.clients.list/create/get/update/delete/rotateSecret/v1/identity/clients
mr.identity.inbox.get(inboxId) · update(inboxId, { enabled }) · signIns(inboxId)/v1/inboxes/:id/identity
verifyAgentIdentityToken(token, { audience, nonce?, replayCache? })For apps: verifies an agent’s ID token against the JWKS (from agentboxd/identity; needs jose)
verifyAgentMessage(message, { recipient, replayCache?, maxAgeSeconds? })Verifies an agent message signature against agent-keys.json (from agentboxd/identity; needs jose, and canonicalize for messages with data)

#Work queue

Delivery is at-least-once: a message whose handler crashed the process comes back after its lease, with a higher delivery_count. Make the handler idempotent. ctx.signal aborts if the lease is lost.

worker.ts
import { Agentboxd } from 'agentboxd';

const mr = new Agentboxd();
const stop = new AbortController();
process.on('SIGTERM', () => stop.abort()); // finish running work, then exit

await mr.inboxes.consume(
  inboxId,
  async (message, ctx) => {
    await handleTicket(message, { signal: ctx.signal }); // acked when this returns, nacked if it throws
  },
  { concurrency: 4, enriched: true, signal: stop.signal },
);

#Contacts and knowledge

contacts.ts
// Who is this? Name, notes, metadata and labels, created from mail.
const c = await mr.contacts.byAddress('priya.n@gmail.com');
console.log(c.notes, c.metadata.crm_id); // "Wholesale…" "hs_48213"

// Link it to your CRM and remember something. Metadata merges; null deletes a key.
await mr.contacts.update(c.id, {
  notes: `${c.notes ?? ''}
Asked about order 1042 on 25 Sep.`,
  metadata: { crm_id: 'hs_48213', plan: 'wholesale' },
  add_labels: ['customer'],
});

// Find contacts by your own id.
const { data } = await mr.contacts.list({ metadata: { crm_id: 'hs_48213' } });
knowledge.ts
// Workspace-wide (no inbox_id) or for one inbox.
await mr.knowledge.create({
  title: 'Shipping times',
  body: '- Germany: 1–2 working days (DHL)
- EU: 3–5 working days',
  inbox_id: inbox.id,
});

// Ranked full-text search. With inbox_id, workspace-wide documents are included too.
const { data: hits } = await mr.knowledge.search('how long does shipping to France take', { inbox_id: inbox.id });
// [{ id, title: 'Shipping times', inbox_id, rank: 0.61, snippet: '…EU: 3–5 working days…' }]

#Errors

Non-2xx responses throw AgentboxdError (also exported as MailroomError) with status, code and message from the API’s error body.

errors.ts
import { AgentboxdError } from 'agentboxd';

try {
  await mr.messages.send(inbox.id, { to: 'x@example.com', subject: 'Hi', text: '…' });
} catch (e) {
  if (e instanceof AgentboxdError && e.code === 'daily_send_limit_exceeded') {
    // back off until 00:00 UTC
  }
}

#Webhook verification

verifyWebhook(signature, timestamp, rawBody, secret, { toleranceSeconds? }) returns a boolean. See Webhooks for a full Express example.