Docs
Open in panel → /app · sending
HTTP send API
POST /v1/send accepts an API key in Authorization: Bearer <api_key> (or
X-Api-Key: <api_key>) and runs the same runSend pipeline as the panel.
Request
curl -X POST https://<appliance>/v1/send \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer pmh_<api_key>' \
-d '{
"from": "ops@verified.example.com",
"to": ["alice@example.com", "ops@example.com"],
"cc": ["audit@example.com"],
"subject": "Hello",
"text": "Hi Alice,\n\nSent via PostMTA Hosted.\n",
"idempotency_key": "2026-07-12-roll-123"
}'
| Field | Required | Notes |
|---|---|---|
from |
yes | email; domain must be verified or active when REQUIRE_VERIFIED_DOMAIN (default on in production) |
to |
yes | string or string[] |
cc, bcc |
no | string[] |
subject |
yes | min 1 char |
text |
yes* | one of text / html required |
html |
yes* | one of text / html required |
idempotency_key |
no | recommended 8–128 chars; unique per workspace; replay returns 409 |
Responses
| Code | Error code | When |
|---|---|---|
200 |
— | Kumo 2xx; credits committed |
400 |
no recipients |
to empty after trim/dedupe |
400 |
DOMAIN_UNVERIFIED |
from is malformed (no @) |
401 |
— | missing / bad API key |
402 |
INSUFFICIENT |
wallet cannot cover recipient count |
403 |
DOMAIN_UNVERIFIED |
production + REQUIRE_VERIFIED_DOMAIN and From domain not verified |
409 |
DUPLICATE |
same idempotency_key was already accepted/rejected |
422 |
(verify only) | domain verify failure — {checks, missing[]} |
429 |
— | token-bucket rate limit hit (60/min per IP for /v1/send) |
Idempotency contract
Per docs/credits.md, the idempotency key is unique per
workspace on credit_ledger. Replay sequence:
holdwith the same key →409 DUPLICATE.holdthencommit→ future replays return 200 + the cached credit disposition (we don’t re-inject ifcommitalready happened).holdthenvoid→ credit released; future replays fresh-hold.
Always supply idempotency_key if you retry on transient network errors.
Verified From admission
When REQUIRE_VERIFIED_DOMAIN=1 (production default) and the From domain
is not in domains.status IN ('verified','active') for the caller
workspace, the route returns:
{ "error": "from domain is not verified", "code": "DOMAIN_UNVERIFIED", "domain": "<...>" }
Override: REQUIRE_VERIFIED_DOMAIN=0 is the emergency opt-out only —
never persist it in .env.production.example.
Rate limit
Token-bucket: 60 requests/min per IP for /v1/send. The panel /v1/panel/send
shares the same limiter scoped to JWT/workspace.
Related
- docs/credits.md — credit lifecycle.
- docs/sending.md —
runSendflow. - docs/smtp.md — SMTP-equivalent path.
- docs/domains.md — verified From gate.
Last verified: Sat Jul 11 2026 17:00:00 GMT-0700 (Pacific Daylight Time). Cross-link maintained byscripts/check-docs-drift.sh (M54).