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.
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:
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 aBuffer/Uint8Arrayfor 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).
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
datafield 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 APIdataobject; 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:
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:
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" },
],
});