Inbound Email

Receive and process incoming emails. Two routing models: legacy `inbound_domains` (self-hosted, custom provider config) and modern `cloud_domains` (cloud edition, dual-region SES with SNS).

Posthawk supports two inbound routing models:

If you registered the domain via POST /v1/domains and called POST /v1/domains/:id/receiving/enable, Posthawk handles inbound routing automatically:

  • Creates a BYODKIM identity in eu-west-1 (Stockholm doesn't support SES inbound, so cloud always routes through Ireland regardless of your sending region)
  • Adds an SES receipt rule that forwards emails to the posthawk-inbound SNS topic
  • The worker's /webhooks/sns/inbound endpoint validates SNS signatures, parses the email, and inserts into inbound_emails with cloud_domain_id set
  • If a webhook URL is configured on the domain (PATCH /v1/domains/:id/webhook), Posthawk POSTs the parsed email to your URL with HMAC signature

You configure everything via the Domains API — no separate inbound domain setup needed.

2. Inbound Domains (legacy / self-hosted)

For self-hosted users or custom routing, the original /inbound/domains API lets you register a domain with a provider:

  • cloudflare — Cloudflare Email Routing forwards to your Posthawk instance (requires MX records pointing to Cloudflare)
  • aws_ses — AWS SES receipt rule forwards to an SNS topic, then a webhook
  • custom — bring your own MTA / DIY setup, just hit the inbound webhook directly

Configure DNS, verify the domain, then incoming emails are parsed and optionally forwarded to your webhook URL.

Common to both

The inbound_emails table has a CHECK constraint requiring exactly ONE of inbound_domain_id or cloud_domain_id to be set per row, so you can tell which routing path delivered the email. Webhook deliveries to your URL include an HMAC signature in X-Posthawk-Signature (same format as outbound webhooks); verify it with your domain's webhook secret.

POST/inbound/domains

Register a domain for inbound email processing.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your JWT.

Body

domainstringrequired

Fully qualified domain name

providerstringoptional

"cloudflare" | "aws_ses" | "custom". Defaults to "cloudflare".

webhookUrlstringoptional

URL to forward parsed emails to

POST /inbound/domains
curl -X POST https://your-posthawk-instance.com/inbound/domains \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your_jwt_token" \
  -d '{
    "domain": "mail.yourdomain.com",
    "provider": "cloudflare",
    "webhookUrl": "https://yourapp.com/api/incoming-email"
  }'
Response
{
  "success": true,
  "data": {
    "id": "uuid",
    "domain": "mail.yourdomain.com",
    "provider": "cloudflare",
    "is_verified": false,
    "webhook_url": "https://yourapp.com/api/incoming-email"
  },
  "message": "Inbound domain created. Configure MX records to start receiving emails."
}
GET/inbound/domains

List all configured inbound domains.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your JWT.

GET /inbound/domains
curl https://api.posthawk.dev/inbound/domains \
  -H "Authorization: Bearer your_jwt_token"
Response
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "domain": "mail.yourdomain.com",
      "provider": "cloudflare",
      "is_verified": true,
      "is_active": true
    }
  ]
}
POST/inbound/domains/:id/verify

Verify domain ownership after DNS records are configured.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your JWT.

Path Parameters

idstringrequired

Domain UUID

POST /inbound/domains/:id/verify
curl -X POST https://api.posthawk.dev/inbound/domains/{id}/verify \
  -H "Authorization: Bearer your_jwt_token"
Response
{
  "success": true,
  "data": { "id": "uuid", "is_verified": true },
  "message": "Domain verified successfully"
}
GET/inbound/domains/:id

Get details of a specific inbound domain.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your JWT.

Path Parameters

idstringrequired

Domain UUID

GET /inbound/domains/:id
curl https://api.posthawk.dev/inbound/domains/{id} \
  -H "Authorization: Bearer your_jwt_token"
Response
{
  "success": true,
  "data": {
    "id": "uuid",
    "domain": "mail.yourdomain.com",
    "provider": "cloudflare",
    "is_verified": true,
    "is_active": true,
    "webhook_url": "https://yourapp.com/api/incoming-email"
  }
}
PATCH/inbound/domains/:id

Update an inbound domain — change the webhook URL or toggle active status.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your JWT.

Path Parameters

idstringrequired

Domain UUID

Body

webhookUrlstringoptional

New webhook URL for email forwarding

isActivebooleanoptional

Enable or disable inbound processing for this domain

PATCH /inbound/domains/:id
curl -X PATCH https://your-posthawk-instance.com/inbound/domains/uuid \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your_jwt_token" \
  -d '{
    "webhookUrl": "https://yourapp.com/api/new-webhook",
    "isActive": true
  }'
Response
{
  "success": true,
  "data": {
    "id": "uuid",
    "domain": "mail.yourdomain.com",
    "webhook_url": "https://yourapp.com/api/new-webhook",
    "is_active": true
  },
  "message": "Inbound domain updated"
}
DELETE/inbound/domains/:id

Delete an inbound domain and stop receiving emails for it.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your JWT.

Path Parameters

idstringrequired

Domain UUID

DELETE /inbound/domains/:id
curl -X DELETE https://api.posthawk.dev/inbound/domains/{id} \
  -H "Authorization: Bearer your_jwt_token"
Response
{
  "success": true,
  "message": "Inbound domain deleted"
}
GET/inbound/emails

List received inbound emails with optional filtering and pagination.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your JWT.

Query Parameters

domainIdstringoptional

Filter by inbound domain

limitnumberoptional

Pagination limit

offsetnumberoptional

Pagination offset

GET /inbound/emails
curl https://api.posthawk.dev/inbound/emails \
  -H "Authorization: Bearer your_jwt_token"
Response
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "from_address": "sender@example.com",
      "to_addresses": ["you@yourdomain.com"],
      "subject": "Hello!",
      "received_at": "2025-06-01T12:00:00Z"
    }
  ],
  "total": 1
}
GET/inbound/emails/:id

Get the full details of a specific inbound email including body, headers, and attachments.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your JWT.

Path Parameters

idstringrequired

Inbound email UUID

GET /inbound/emails/:id
curl https://api.posthawk.dev/inbound/emails/{id} \
  -H "Authorization: Bearer your_jwt_token"
Response
{
  "success": true,
  "data": {
    "id": "uuid",
    "from_address": "sender@example.com",
    "from_name": "John Doe",
    "to_addresses": ["you@yourdomain.com"],
    "subject": "Hello!",
    "html": "<p>Email body here</p>",
    "text": "Email body here",
    "attachments": [],
    "received_at": "2025-06-01T12:00:00Z",
    "webhook_status": "delivered"
  }
}