Skip to content

Extend: Scripts — ad-hoc mailers, syncs, and imports

Lane 3 of Extend Tinbox — the programmatic surface (SDK, CLI, MCP) on one token, including named-template render on send.

Lane 3 is the programmatic surface: a script you run on your own machine (or a Worker, a cron box, a laptop) that reaches into your Tinbox instance over HTTP. One SDK, one token, three surfaces — send mail, read your whole deployment over typed tRPC, and call the same agent tools your Assistant uses. This is the lane for the one-off mailer, the nightly CRM sync, the "import my old inbox" chore. Here's the whole thing in fifteen lines: read an audience, render a stored template per recipient, and send — idempotently.

import { Tinbox } from "tinbox/sdk";
const tb = new Tinbox({ host: "https://acme.dev", apiKey: process.env.TINBOX_API_KEY });

// Read a Tinbox audience over the fully-typed tRPC client…
const { members } = await tb.api.audiences.members.query({ audienceId: "aud_weekly" });

// …and send each member a per-recipient render of a stored template, idempotently.
await tb.emails.batch(members.map((m) => ({
  from: "Acme <news@acme.dev>",
  to: m.email,
  subject: "Your weekly digest",
  template: "weekly-digest",             // a template you stored (Settings → Templates)
  data: { FNAME: m.name, TOP_PICK: m.topPick },
  idempotencyKey: `DIGEST-${weekId}-${m.email}`,
})));

That's a real, running program. No template engine to wire up, no merge loop, no dedupe table — the server does the render and the ledger does the deduping.

The SDK

The SDK is tinbox/sdk, imported from the main tinbox npm package — one install, no separate dependency. It is dependency-free (fetch-only) and runs anywhere fetch does: Cloudflare Workers, Node 18+, browsers, Bun, Deno.

Construct it with your instance origin and a token:

import { Tinbox } from "tinbox/sdk";

const tb = new Tinbox({
  host: "https://acme.dev",             // your instance origin
  apiKey: process.env.TINBOX_API_KEY,   // one tbx_… capability token
});

host is your deployment's origin; the SDK derives /api/send and /api/trpc from it. You never build those URLs by hand.

One token, every surface

Auth is one tbx_… capability token. Create it in Settings → API keys, or from the CLI:

tinbox tokens create-send-key

The same token works for the SDK send surface, the typed tRPC client (tb.api.*), the CLI, and the /mcp endpoint. Scope it to what the script needs — one identity, one domain, or the whole account.

Three surfaces hang off the one client:

  • tb.emails.* — Resend-style send / batch / get. The transactional lane.
  • tb.api.* — the fully typed tRPC client over your entire deployment: audiences, contacts, threads, campaigns, settings. Autocomplete and types come straight from your instance's router.
  • tb.tools.* — aliases for the generated agent tools, so a script can call exactly what the Assistant calls.

Named-template render on send

The template + data pair on emails.send / emails.batch is the newest piece. Instead of shipping html from your script, you pass:

  • template — a stored template's id or name (e.g. "weekly-digest").
  • data — merge-tag values keyed by tag, e.g. { FNAME: "Ada", TOP_PICK: "…" }.

The server looks up the stored template, substitutes merge tags (*|FNAME|*, conditionals) per recipient, and sends. Store templates in Settings → Templates or via the templates API.

await tb.emails.send({
  from: "Acme <news@acme.dev>",
  to: "ada@example.com",
  subject: "Welcome",
  template: "welcome",
  data: { FNAME: "Ada" },
});

Two rules worth internalizing:

  • template and html are mutually exclusive. Send both and you get a 422 — no guessing which one wins.
  • A missing template is a hard error, never a silent empty send. If the server doesn't have the named template, it rejects the send with a clear error. You'll never blast a batch of blank emails because a name was mistyped.

Idempotency

Pass idempotencyKey for at-most-once business sends. A retry with the same key is a safe replay against the send ledger — the message goes out once, and a re-run of your script (a crashed batch, a cron double-fire) doesn't double-mail anyone. In the opener, DIGEST-${weekId}-${m.email} gives every recipient a stable key for the week, so the whole batch is re-runnable.

Both camelCase and snake_case are accepted throughout — replyTo/reply_to, idempotencyKey/idempotency_key — so paste from either convention and it just works.

Thread directives

The reply-and-drip directives from Lane 2 work here too: onReply and drip on a send set up automated thread behavior driven from your script. See Extend: Replies for the full grammar.

For everything the send surface accepts — attachments, headers, scheduling, tags — the fuller reference is the Sending API guide.

The CLI and MCP

The tinbox bin is the scripting lane's on-ramp for bulk moves. Its import tooling pulls existing mail and contacts into your instance:

tinbox import-gmail
tinbox import-imap
tinbox import-mailchimp
tinbox import-mbox

These are the ad-hoc scripting entry points for "migrate me onto Tinbox" — run once, glue into a script, or schedule.

MCP: the /mcp endpoint exposes the manifest tools to the Claude app on the same token. Point Claude at your instance and it can call the same tb.tools.* surface conversationally.

Which lane instead?

Scripts run outside the worker, reaching in over HTTP. If that's not the shape you need:

  • To run code inside the worker — a webhook, a cron job, a custom route — that's Plugins (lane 4).
  • To give the Assistant a new callable tool — a query or action it can invoke in conversation — that's Views (lane 5).

All six lanes, side by side, live on the Extend Tinbox hub.