Sending mail
All authenticated sends go through one pipeline: route + kind checks, suppression lookup, content gate, unsubscribe headers, then the outbound provider.
Endpoints
| Method | Path | Notes |
|---|---|---|
POST |
/api/practices/{id}/send |
One message. 202 { message_id, id, status } |
POST |
/api/practices/{id}/send/batch |
Up to 500 messages |
POST |
/api/practices/{id}/send/bulk |
One template, many recipients ({{var}}) |
POST |
/api/practices/{id}/send/score |
Same gate as send, no delivery |
POST |
/api/practices/{id}/send-test |
Quality / mailbox-simulator kinds |
Idempotency: send Idempotency-Key (or X-Idempotency-Key) on single send. Batch items may include idempotency_key. Replays return the original response.
Body shape
Accepted aliases: text / text_body, html / html_body. to may be a string or an array.
{
"from": "Praxis <noreply@your-domain.example>",
"to": ["patient@example.com"],
"cc": [],
"bcc": [],
"reply_to": ["praxis@example.com"],
"subject": "Ihr Termin",
"text": "Plain text",
"html": "<p>HTML</p>",
"route": "outgoing",
"message_kind": "termin",
"tag": "appointment",
"tags": ["appointment", "reminder"],
"metadata": { "patient_id": "42" },
"settings": { "track_opens": false, "track_clicks": false }
}
Limits: ≤50 recipients (to+cc+bcc). Addresses in more than one bucket are deduped (To wins over Cc over Bcc).
Attachments
Base64 attachments[]. Caps: 5 files, 10 MiB decoded each, combined decoded ≤ ~30 MiB, HTTP body ≤ ~48 MiB.
Allowed: pdf, office documents, csv/tsv, ics, png/jpg/gif/webp. Archives and executables (zip, rar, exe, js, html, …) and suspicious double extensions are rejected. CSV/TSV always download as attachments.
Inline images: set content_id and reference cid:your-id in HTML. Bare ids are normalized.
GET /api/meta exposes attachment_policy so clients can enforce the same rules before upload.
Calendar (Termin)
Include calendar (summary, starts_at, optional ends_at / location) to attach a METHOD:REQUEST .ics. Preview with POST …/integrations/ics-preview.
Content reputation gate
Before send, Werkmail scores subject, body, HTML, and attachments. Optional scanner sidecar and hostname blocklists can add signals.
- Local score ≥ 70, scanner reject, virus, or listed host → hard block (
content_blockedin the JSON error). The message is stored asfailedand webhooksmessage.failed+message.content_blockedfire. - Softer issues return
content_warningswithout blocking.
POST …/send/score returns score, grade, inbox_risk, would_block, reasons, warnings, and recommendations — use it in compose UIs.
Unsubscribe on every send
When the public origin is HTTPS, every message (test, transactional, marketing) gets:
- A subtle footer link to opt out of all mail from this sender
List-UnsubscribeandList-Unsubscribe-Post: List-Unsubscribe=One-Click
Broadcast / newsletter also requires a complete Impressum (street, city, privacy URL) and injects a legal footer.
Marketing eligibility
Broadcast requires:
- DKIM verified
- Warm-up day ≥ 3
- Complete Impressum
- Active newsletter consent (DOI) for list sends
Transactional routes reject promotional-looking content.
Message-ID and platform signatures
Werkmail sets the RFC 5322 Message-ID to your From domain (raw MIME). Recipients who open “original message” should see @your-domain, not a platform host.
The outbound platform may still add its own DKIM signature in the raw source (d= on a platform domain). You cannot replace that signature. Inbox authentication uses your domain DKIM (d=your-domain) once those CNAMEs are verified. Custom MAIL FROM (bounce.<domain>) brands the envelope Return-Path the same way.
See Raw source and branding for the full table of what we brand, what the platform always adds, and the SMTP-relay escape hatch.
Safe mode
While safe mode is on, recipients must be the mailbox simulator, a placement tester, or an allowlisted address. GET /api/meta reports the current flag. Production tenants turn this off after go-live (GET …/cutover lists remaining checks).