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-stylesend/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:
templateandhtmlare 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.