Skip to content

API keys

Mint bearer API keys and send mail programmatically through the same pipeline as the UI.

Updated View as Markdown

Doota can send mail from scripts, agents, and other apps via a bearer API key. A key goes through the same authorization (can() mailbox SEND capability) and the same submission pipeline as an interactive session — there’s no parallel permission path.

Service accounts

Programmatic sending happens through a service account — a mailbox (notifications@, billing@) that your apps send as, with no human owner, so its keys survive staff changes. A service account owns three things: keys, templates, and a send log.

Anyone with manage access to the mailbox administers it in-app: open it from the mailbox switcher’s Manage mailbox link → API keys / Send log tabs. No admin dashboard required.

Mint a key

From the service account’s API keys tab, choose New key. Give it a name (required) — name it after where it runs (CI deploy, billing worker) so you can recognise it in the list later. A key:

  • Is presented as a bearer token prefixed dk_…shown once at creation, so copy it immediately. Only a hash is stored; it can never be shown again.
  • Authorizes the service mailbox directly (no owning user), and may only send as that mailbox.

To let a teammate send too, give them manage access to the service account — they mint their own key. You never re-share a secret.

Send a message

POST /api/send with Authorization: Bearer dk_… and a JSON body.

Send via curlsh
curl -X POST https://mail.acme.com/api/send \
  -H "Authorization: Bearer dk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "mailboxId": "mbx_123",
    "to": ["someone@example.com"],
    "cc": [],
    "subject": "Hello from Doota",
    "text": "Plain-text body.",
    "html": "<p>Or an <b>HTML</b> body.</p>",
    "idempotencyKey": "order-4571-receipt"
  }'

A successful call returns 202 Accepted:

{ "submissionId": "sub_…", "deduped": false }

The message is now in the outbound pipeline; track its queued → sending → sent → delivered (or bounced) status by the returned submissionId.

Send with a template

Instead of subject/html, pass a templateId and its data. Doota renders the hosted template with your merge variables (Jinja {{ name }}, auto-escaped) and sends the result:

Templated sendsh
curl -X POST https://mail.acme.com/api/send \
  -H "Authorization: Bearer dk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["ana@example.com"],
    "templateId": "tmpl_welcome",
    "data": { "name": "Ana", "code": "1234" },
    "idempotencyKey": "welcome-user-9012"
  }'

Request body

Field Type Notes
mailboxId string Required unless the key is mailbox-scoped. If the key is bound, this must match the bound mailbox.
to / cc / bcc string[] At least one recipient across the three is required.
templateId string Send a hosted template. Overrides subject/html.
data object Merge variables for the template.
subject string Raw send. Defaults to empty. Ignored when templateId is set.
text string Plain-text body (or null). Ignored when templateId is set.
html string HTML body (or null). Ignored when templateId is set.
fromAliasId string Send as an alias instead of the mailbox’s primary address.
parentMessageId string Thread the send onto an existing message (a reply).
sendAt number Unix epoch (ms) for a scheduled send. Released on the next 5-minute cron tick.
idempotencyKey string Dedupes retries. If omitted, a random one is generated — so always set your own for safe retries.
attachments object[] Files to attach — see Attachments. Each is { filename, content } (base64) or { filename, url }.

Attachments

Attach files with an attachments array. Each entry has a filename and exactly one of:

  • content — the file’s bytes as a base64 string (the SDK base64-encodes a Buffer / Uint8Array for you). Best for files you generate in-memory.
  • url — a public URL Doota fetches server-side. The fetch is SSRF-guarded (private / loopback / link-local hosts are rejected, re-checked on every redirect). Best for files already hosted somewhere.

Optionally set contentType (defaults to the fetched type, or application/octet-stream).

Send with attachmentssh
curl -X POST https://mail.acme.com/api/send \
  -H "Authorization: Bearer dk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["ana@example.com"],
    "subject": "Your receipt",
    "html": "<p>Thanks — receipt attached.</p>",
    "attachments": [
      { "filename": "receipt.pdf", "content": "JVBERi0xLjQK…" },
      { "filename": "logo.png", "url": "https://cdn.acme.com/logo.png" }
    ],
    "idempotencyKey": "receipt-4571"
  }'

Limits: up to 20 files, 25 MB each, 40 MB total per send. Bytes are encrypted at rest and ride the same pipeline as the message body; attachment contents are never written to the send log.

Idempotency

Reusing an idempotencyKey returns the original submission with "deduped": true instead of sending twice. Set a stable key per logical message (an order id, a notification id) so a network retry never double-sends.

Errors

Status Meaning
401 Missing or invalid API key.
403 Key is mailbox-scoped and mailboxId doesn’t match its bound mailbox.
404 templateId doesn’t exist for this account (or is archived).
400 Bad JSON, missing mailboxId, or no recipients.
500 Outbound mail isn’t configured on the Worker (missing bindings/secrets).

Templates

Build reusable emails once, send them with data. From Templates (sidebar), create a template in the WYSIWYG editor — write directly on the email canvas, press / for a command menu, or use the floating insert rail (grouped Text · Media · Layout · Variables). Blocks include headings (Title / Subtitle / Heading), text and lists, buttons, images, 2–4 column rows, a hero (full-width background image with heading + button), social links, a footer (address + unsubscribe line), dividers, spacers, and raw-HTML blocks. Image and hero blocks accept a URL or an upload (PNG/JPEG/GIF/WebP, 2 MB max — served from a public, cached URL so recipients’ clients can load it).

Select any block to style it in the right panel: buttons take a fill / text colour / size / radius / full-width, images a width + alignment, the hero a text colour + height, and every block a background / padding / border.

The Theme panel sets default typography per text type (Text / Title / Subtitle / Heading → colour, size, weight, line-height) so the whole email stays consistent without touching each block. Page style sets the body background colour and width; Global CSS and each block’s own Custom CSS are a full code editor (CodeMirror) with highlighting, autocomplete, and bracket matching. (CSS compiles to an mj-style block — many email clients ignore <style>, so prefer the block controls where you can.)

Put {{ variable }} tags anywhere for merge data. Hit Preview (eye icon) to render the email with sample values — toggle Desktop / Mobile, and Send a test to your own inbox to check it end-to-end before going live.

Under the hood the builder is Svelte-native and open-source: your blocks compile to MJML (an email layout language that works across mail apps) via MRML — a fast WebAssembly compiler — and at send time the {{ variables }} are filled in by a lightweight template engine (un-jinja). Editing a template creates a new version; each send pins the version it used, so the log reproduces exactly what went out.

Variables

Two kinds, both written as {{ name }}:

  • Provided — Doota fills these automatically; you don’t send them (and they override any data field of the same name):

    Variable Value
    {{ recipient }} the recipient’s email address
    {{ sender_name }} the sending mailbox’s display name
    {{ sender_email }} the sending mailbox address
    {{ year }} current year
    {{ date }} current date (YYYY-MM-DD)
    {{ unsubscribe_url }} the recipient’s unsubscribe link (host taken from the sending domain; path from UNSUBSCRIBE_URL, default /unsubscribe)
  • Yours — any other {{ name }} you use. Supply them in the API data object; missing values render empty. The builder’s Variables panel lists both (click a chip to copy its tag).

Send log

Every API send from a service account is recorded under its Send log tab: timestamp, recipients, subject, which key, and which template. The merge data you passed is encrypted at rest and auto-expires after 30 days (configurable), then only the metadata remains — you keep an audit trail without a permanent copy of personal details. Variables you flag sensitive are never logged at all.

Client SDK

A Resend-shaped client wraps the endpoint:

@doota/sdkts
import { Doota } from "@doota/sdk";

const doota = new Doota("dk_your_key_here", { baseUrl: "https://mail.acme.com" });

await doota.emails.send({
  to: "ana@example.com",
  templateId: "tmpl_welcome",
  data: { name: "Ana", code: "1234" },
});

It returns { submissionId, deduped } and throws a DootaError (with the HTTP status) on failure. Raw sends work too — pass subject + text/html instead of templateId/data.

Attachments take a Buffer/Uint8Array (base64-encoded for you) or a url:

With attachmentsts
await doota.emails.send({
  to: "ana@example.com",
  subject: "Your receipt",
  html: "<p>Receipt attached.</p>",
  attachments: [
    { filename: "receipt.pdf", content: pdfBuffer },
    { filename: "logo.png", url: "https://cdn.acme.com/logo.png" },
  ],
});
Navigation

Type to search…

↑↓ navigate↵ selectEsc close