Send API
Set BASE=https://werkmail.eu. Authenticate with a session, team, or project bearer.
Single send
POST /api/practices/{id}/send → 202
curl -sS -X POST "$BASE/api/practices/$PRACTICE_ID/send" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123" \
-d '{
"from": "Praxis <noreply@your-domain.example>",
"to": ["you@example.com"],
"reply_to": ["praxis@example.com"],
"subject": "Hello from Werkmail",
"text": "Plain text body",
"html": "<p>HTML body</p>",
"tag": "appointment",
"route": "outgoing",
"message_kind": "transactional",
"metadata": {"patient_id": "42"}
}'
Response:
{ "message_id": "uuid", "id": "uuid", "status": "queued" }
Blocked content returns 4xx with code content_blocked, content_reasons, and scanner.
Fields
| Field | Type | Notes |
|---|---|---|
from |
string | Optional; defaults to the tenant From |
to |
string or string[] | Required |
cc, bcc, reply_to |
string or string[] | Optional |
subject |
string | Required |
text / text_body |
string | One of text or html required |
html / html_body |
string | |
route |
string | Slug or UUID; default outgoing |
message_kind |
string | termin, recall, doi, transactional, newsletter, custom, … |
tag |
string | First tag; used in filters |
tags |
string[] | Normalized case-insensitive unique |
metadata |
object | String values; echoed on webhooks |
headers |
object | Extra MIME headers (restricted set) |
attachments |
array | See Sending |
settings.track_opens |
bool | Override route default |
settings.track_clicks |
bool | Override route default |
calendar |
object | ICS invite (summary, starts_at, …) |
idempotency_key |
string | Same as the header |
Recipient cap: 50 (to+cc+bcc). Dedup across buckets.
Batch
POST /api/practices/{id}/send/batch — JSON array, max 500.
curl -sS -X POST "$BASE/api/practices/$PRACTICE_ID/send/batch" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '[
{"to":"a@example.com","subject":"One","text":"hi","idempotency_key":"order-1","tags":["orders"]},
{"to":"b@example.com","subject":"Two","text":"hi","idempotency_key":"order-2","tag":"receipt"}
]'
Bulk (templated)
POST /api/practices/{id}/send/bulk and POST /api/emails/send-bulk (flat; pass project_id).
Placeholders use {{variable}}. Each recipient is rendered locally and sent through the shared pipeline. Response is a per-recipient results[] array.
curl -sS -X POST "$BASE/api/emails/send-bulk" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: campaign-2026-03-20" \
-d '{
"project_id": "'"$PRACTICE_ID"'",
"template": {
"subject": "Hello {{name}}",
"html": "<p>Hi {{name}}, your code is {{code}}.</p>"
},
"recipients": [
{"email":"a@example.com","data":{"name":"Ada","code":"42"}},
{"email":"b@example.com","data":{"name":"Grace","code":"7"}}
]
}'
Defaults: message_kind=newsletter, route broadcast. List-Unsubscribe is always injected.
Score (dry-run)
POST /api/practices/{id}/send/score — same body as send, no delivery.
Returns score, grade, inbox_risk, would_block, reasons, warnings, scanner, recommendations, and broadcast/Impressum eligibility when the route is marketing.
Test send
POST /api/practices/{id}/send-test
{ "kind": "quality", "to": "you@example.com" }
kind: quality | success | bounce | complaint | ooto
success / bounce / complaint / ooto target the mailbox simulator. quality sends a placement-oriented sample to the address you provide (allowlisted in safe mode).