One REST surface across email, WhatsApp, and SMS. Idempotency keys, signed webhooks, trace IDs in every send, edge-runtime compatible when SDKs land. Today, the API speaks fluent curl in every language you ship with.
The whole API is REST + JSON. Pick the language you ship with — the request shape is the same across all of them. Native SDKs (npm · pip · go get · gem · composer) are Q3 2026.
# 1. Send a transactional email — one POST
curl https://api.emaildigit.com/api/campaigns/send \
-H "Authorization: Bearer $ED_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "amelia@stripe.com",
"subject": "Welcome to Acme",
"html": "<p>Hi Amelia, your account is ready.</p>"
}'
# →
{
"send_id": "snd_8f3k2p4xN",
"status": "queued",
"trace_id": "tx_aWelcomeEmail9k2"
}Idempotency, retries, signing, traces. Same patterns as Stripe or Plaid — minus the email-specific cruft you'd otherwise wire up by hand.
Per-workspace bearer credentials. Generate from the dashboard, copy once, store in your secrets manager. Tokens self-identify with the ed_live_ prefix so GitHub secret scanning catches accidental commits.
Sign payloads with sha256({ts}.body). Receivers verify in 10 lines (snippet above). Replay defense via 5-minute timestamp window.
1m → 5m → 30m → 2h → 12h → dead. Auto-pause after 25 consecutive failures so a permanently-broken endpoint stops eating queue capacity.
Every state change emits an append-only audit entry. Surfaces in /dashboard/audit and via API. SOC 2 evidence built-in.
Pass Idempotency-Key: <your-uuid> on any POST. Same key inside 24h returns the prior response — no duplicate sends, no charge.
Each ed.send() returns a trace_id that follows the message through delivery → opens → clicks → replies. Pipe it into Datadog / Honeycomb / your OTEL stack.
Every event is HMAC-SHA256-signed. Verify the signature, drop unsigned requests, dispatch on event type. Two of the most-deployed examples below — same pattern across every other language.
// Verify the X-Email-Digit-Signature header on inbound deliveries
import crypto from "crypto";
import express from "express";
const SECRET = process.env.ED_WEBHOOK_SECRET;
function verify(rawBody, header) {
const [sigPart, tsPart] = header.split(",");
const sig = sigPart.slice("sha256=".length);
const ts = tsPart.slice("t=".length);
// Reject anything older than 5 minutes (replay defense)
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${ts}.`)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
// IMPORTANT: use express.raw — JSON parsers re-serialize and break the MAC
app.post("/webhooks/email-digit",
express.raw({ type: "application/json" }),
(req, res) => {
if (!verify(req.body, req.get("X-Email-Digit-Signature"))) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
// dispatch on event.type
res.status(200).send("ok");
});Subscribe to all of them with `*`, or just the ones you need. Same payload envelope across every type.
reply.receivedA reply lands in the workspace (email or WhatsApp), after classification.email.sentAn outbound email is accepted by the relay.email.bouncedAn outbound email fails at the relay (4xx/5xx/timeout).domain.verifiedA sending domain transitions to `verified` status.domain.failedA previously-verified domain transitions back to `pending`.mailbox.connectedA new inbox is connected (Gmail / Outlook / IMAP).whatsapp.receivedAn inbound WhatsApp message arrives (also fires `reply.received`).test.pingSent when you click `Send test` in the dashboard. Useful for setup verification.127.0.0.1:8001Run our backend locally with uv + Postgres. Five-minute setup; full feature parity.
api-dev.emaildigit.comAuto-deploys from the dev branch. Connects to a Neon branch DB. Safe to break.
api.emaildigit.comAuto-deploys from main. Real customer data. The ed_live_ token tells you you're hitting it.
Q3 2026 for TypeScript + Python. Go, Ruby, PHP, Rust follow. Until then the REST API is small enough to wrap in a 100-line module per language — and we'll publish those wrappers as community-maintained references along the way.
Per-token: 100 req/s burst, 10 req/s sustained, 100k req/day. Per-workspace: 10× that. Free tier: half those numbers. We return X-RateLimit-Remaining on every response and 429 with Retry-After when you cross it. Email sends count separately — those are governed by your tier's monthly cap.
Yes — the REST API works from anywhere that can make HTTPS requests. Once SDKs ship they'll be edge-compatible (no Node-only deps).
Tunnel to your laptop with ngrok or Cloudflare Tunnel, paste the URL into /dashboard/webhooks, click Send test. You'll get a test.ping event within ~30 seconds. Replay any delivery from the dashboard.
Endpoints live under /api/ today (no version prefix). When we ship v1 we'll keep the current routes alive for a year minimum. Breaking changes are announced in the changelog.
Revoke it in /dashboard/api-tokens — takes effect within 30 seconds. Mint a new one. The audit log records every request the leaked token made, so you can scope blast-radius investigations. GitHub's secret scanner detects ed_live_ prefixes and alerts you within minutes if you accidentally push one to a public repo.
Request access, mint a token, paste a curl. Most teams have their first send delivered before the kettle boils.