← Back to home

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


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)

Need one of these? Contact the operator.