Skip to main content

Receiving Email

Upstream providers (or your own MX) deliver normalized messages to Posta's public ingestion webhook. These endpoints are authenticated with the opaque inbound secret, not a JWT or API key.

Ingestion Webhook

POST /api/v1/inbound/webhook

Authenticate with the shared secret from POSTA_INBOUND_WEBHOOK_SECRET, sent in the X-Posta-Inbound-Secret header. The comparison is constant-time. If inbound is not configured (empty secret), the endpoint returns 403.

Request Body

The payload is a normalized JSON envelope under a top-level body object:

{
"body": {
"from": "sender@example.com",
"to": ["inbox@yourdomain.com"],
"subject": "Hello",
"text": "Plain text body",
"html": "<p>HTML body</p>",
"headers": { "X-Custom": "value" },
"message_id": "<abc123@example.com>",
"spam_score": 0.1,
"attachments": [
{
"filename": "invoice.pdf",
"content_type": "application/pdf",
"content": "<base64-encoded bytes>"
}
],
"raw": "<optional base64-encoded raw RFC 5322 message>"
}
}
FieldTypeRequiredDescription
fromstringyesSender address.
toarrayyesRecipient addresses. At least one must match a verified domain.
subjectstringnoMessage subject.
textstringnoPlain-text body.
htmlstringnoHTML body.
headersobjectnoMap of header name to value.
message_idstringnoRFC 5322 Message-ID; angle brackets are stripped. Used for deduplication.
spam_scorenumbernoProvider-supplied spam score.
attachmentsarraynoAttachments with base64-encoded content.
rawstringnoBase64-encoded raw .eml, stored for later export.
curl -X POST http://localhost:9000/api/v1/inbound/webhook \
-H "X-Posta-Inbound-Secret: <inbound_secret>" \
-H "Content-Type: application/json" \
-d '{
"body": {
"from": "sender@example.com",
"to": ["inbox@yourdomain.com"],
"subject": "Hello",
"text": "Plain text body"
}
}'

Response

On acceptance the endpoint returns 202 Accepted:

{
"accepted": true,
"inbound_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "received"
}

The status field and HTTP code vary by outcome:

HTTPstatusCondition
202receivedMessage accepted and queued for dispatch.
200duplicateA message with the same id was already ingested (idempotent).
202suppressedSender is on the recipient's suppression list; stored, not forwarded.
403Recipient domain is not verified, or inbound is not configured.
413too_largeMessage or attachment exceeds the configured size limit.
401Invalid inbound secret.

Deduplication

Posta deduplicates by message_id per resolved user. If no message_id is supplied, a stable content hash (sender, recipients, subject, and size) is used as a fallback. Re-POSTing a duplicate returns 200 with status: "duplicate" and the original inbound_id.

Downloading Attachments (Signed Token)

When the email.inbound webhook is dispatched, each attachment includes a signed download URL rather than inline bytes. Webhook consumers fetch the content asynchronously:

GET /api/v1/inbound/attachments/{uuid}/{idx}?t={token}
  • {uuid} — the inbound email UUID.
  • {idx} — the zero-based attachment index.
  • t — the HMAC-signed token from the webhook payload.

The token is validated against the server's HMAC key; an invalid or missing token returns 401. The response streams the raw attachment bytes with the original Content-Type and a Content-Disposition filename.

note

This token-signed endpoint is for webhook consumers. Authenticated workspace users can download attachments without a token via the management API.