Email API — sending
Last updated 2026-06-13 — adopter-facing guide for sending email through
https://doable.services.
Send transactional email — notifications, magic links, confirmations — to your users through the doable.services Email API, authenticated with an API key. No SMTP setup, no mail server: one HTTPS request per message.
If you just need to get going, jump to Quick start.
Overview
| Item | Value |
|---|---|
| Base URL | https://doable.services |
| Endpoint | POST /api/v1/email/send |
| Auth | Authorization: Bearer <YOUR_API_KEY> |
| Content type | application/json |
| Sender | Fixed to <your-route>@mg.doable.services (see Your sender) |
You'll be issued an API key with email-sending permission. Keep it secret (server-side only — never ship it in browser/JS that users can view). Anyone with the key can send as your sender address.
Quick start
curl -X POST https://doable.services/api/v1/email/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Your sign-in link",
"html": "<p>Hi! <a href=\"https://example.com/login?t=…\">Click to sign in</a>.</p>"
}'
Your message is sent from your-route@mg.doable.services automatically — you
don't set the sender. Success returns the queued message id:
{ "success": true, "id": "<20260613101319.abc@mg.doable.services>" }
Your sender
On the shared mg.doable.services domain, the sender is fixed to your route:
<your-route>@mg.doable.services
You do not set or change it — there's no from field to fill in, and any
from you send is ignored. This keeps the shared domain's reputation clean and
unambiguous across everyone using it.
Want your own sender (your brand, a custom address, your own domain)? You
need a verified sending domain attached to your route. Contact the operator
to set one up; once attached, you'll be able to choose your from address on
that domain.
Replies: by default mail is sent from
mg.doable.services, which isn't a monitored inbox. If you need replies, ask the operator to enable inbound forwarding for your route (replies are forwarded to your address).
Request
POST /api/v1/email/send with a JSON body:
| Field | Required | Description |
|---|---|---|
to |
yes | Recipient email address. |
subject |
yes | Subject line. |
html |
yes* | HTML body. |
text |
yes* | Plain-text body. |
route |
no | Route slug to send on behalf of. Only needed if your key serves more than one route; with a single-route key it's inferred. |
* Provide html, text, or both. At least one is required. The sender is set
for you (see Your sender) — there is no from field on the
shared domain.
Response & status codes
| Status | Meaning |
|---|---|
200 |
Queued. Body: {"success": true, "id": "<…>"}. |
400 |
Missing/invalid field (to, subject, body), or route needed but not given. |
401 |
Missing or invalid API key. |
403 |
Key lacks send permission, or not authorised for the given route. |
429 |
Rate limit exceeded (see below). |
502 |
Upstream mail provider error — safe to retry with backoff. |
503 |
Email sending is currently disabled. |
Errors return {"success": false, "error": "…"} (or {"error": "…"}).
Rate limits
| Window | Default limit (per key) |
|---|---|
| Per hour | 100 |
| Per day | 1000 |
Exceeding a limit returns 429. Need more for a launch or campaign? Ask the
operator to raise your key's limits.
Examples
Node.js (fetch)
const res = await fetch("https://doable.services/api/v1/email/send", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.DOABLE_EMAIL_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "user@example.com",
subject: "Your sign-in link",
html: "<p>Click to sign in: <a href='…'>Sign in</a></p>",
}),
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
Python (requests)
import os, requests
r = requests.post(
"https://doable.services/api/v1/email/send",
headers={"Authorization": f"Bearer {os.environ['DOABLE_EMAIL_KEY']}"},
json={
"to": "user@example.com",
"subject": "Your sign-in link",
"html": "<p>Click to sign in: <a href='…'>Sign in</a></p>",
},
timeout=15,
)
data = r.json()
if not data.get("success"):
raise RuntimeError(data.get("error"))
Best practices
- One recipient per request. Send individual messages rather than bulk blasts — better deliverability and clearer logs.
- Keep the key server-side. Put it in an environment variable / secret store. If it leaks, ask for a rotation.
- Handle
429/502with exponential backoff and retry; treat4xx(except429) as permanent — fix the request. - Plain-text matters. Include a
textpart alongsidehtmlfor the best deliverability.
Receiving mail (inbox)
If the operator enables inbound storage for your route, mail sent to
anything@<your-route>.doable.services is stored and retrievable with a key
that has read permission (can_read_email). (Inbound can also be forwarded
to an inbox — ask the operator; the two are independent.)
List messages — GET /api/v1/email/inbox
Query params (all optional): route (slug, if your key serves several),
unread=1, limit (default 50, max 200).
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://doable.services/api/v1/email/inbox?unread=1&limit=20"
{
"count": 1,
"messages": [
{ "id": 42, "received_at": "2026-06-13T10:12:00Z", "route": "your-route",
"from": "user@example.com", "recipient": "hi@your-route.doable.services",
"subject": "Hello", "is_read": false, "has_html": true }
]
}
Fetch one — GET /api/v1/email/inbox/<id>
Returns the full message (text + html) and marks it read.
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://doable.services/api/v1/email/inbox/42
Delete — DELETE /api/v1/email/inbox/<id>
curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" \
https://doable.services/api/v1/email/inbox/42
Stored messages are pruned automatically after a retention window (default 30 days). A key only sees mail for routes it's authorised on.
What this API does not do (yet)
- Custom senders require a verified sending domain attached to your route (contact the operator). On the shared domain the sender is fixed.
- Attachments and templates/merge variables are not yet supported.
Need one of these? Contact the operator.