Inbound Webhooks

Process incoming emails with real-time webhooks.

Postscale can parse incoming emails and deliver them to your application via webhooks. This enables powerful use cases like support ticket systems, reply detection, and email-based workflows.

Each accepted inbound message consumes one unit of your shared monthly email quota. Messages rejected before Postscale stores or dispatches them do not consume quota. On Free, monthly quota exhaustion is a hard cap and returns a permanent failure; temporary throughput deferrals count only if a later retry is accepted.

Setup

  1. Add an inbound domain in Domains. For Free accounts, use a subdomain such as inbound.example.com.
  2. Configure that domain or subdomain's MX records to point to Postscale.
  3. From the dashboard, open Webhooks, click Add Webhook, point it at your endpoint URL, and check email.received (any other events you want are also fine — one webhook can subscribe to many).
  4. Copy the signing secret (whsec_…) shown in the dialog. It is only displayed once; save it in your secrets manager.

For root-domain inbound processing, the domain must be configured as inbound, both, or alias, and its MX record must point to mx.postscale.io. If Google Workspace, Microsoft 365, or another mailbox provider receives mail for your root domain, keep that domain outbound in Postscale and use an inbound subdomain for webhooks.

On Free, inbound is scoped to one subdomain with two aliases/routes. Root-domain inbound, catch-all, and combined send/receive domains require a paid plan or manual review.

You can also create the webhook via API:

curl -X POST https://api.postscale.io/v1/webhooks \
  -H "Authorization: Bearer ps_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/postscale-inbound",
    "events": ["email.received"]
  }'

The response body contains the webhook record and the plaintext secret. Save it then — GET /v1/webhooks will not return it again.

Webhook Payload

When an email arrives, Postscale POSTs a JSON body like this to your endpoint:

{
  "event": "email.received",
  "timestamp": "2026-04-25T10:30:00Z",
  "email_id": "0c1f2a3b-…",
  "message_id": "<unique-id@example.com>",
  "from": "customer@example.com",
  "to": "support@yourapp.com",
  "subject": "Help with my order",
  "text": "Hi, I need help with order #12345...",
  "html": "<p>Hi, I need help with order #12345...</p>",
  "headers": [
    { "name": "Message-ID", "value": "<unique-id@example.com>" },
    { "name": "In-Reply-To", "value": "<previous-id@yourapp.com>" }
  ],
	  "attachments": [
	    {
	      "id": "5b7d9c0e-…",
	      "filename": "screenshot.png",
	      "content_type": "image/png",
	      "size": 45678,
	      "url": "https://api.postscale.io/v1/inbound-emails/0c1f2a3b-…/attachments/5b7d9c0e-…/download"
	    }
	  ],
  "spf": "pass",
  "dkim": "pass",
  "auth_results": {
    "dkim": {
      "result": "pass",
      "domain": "example.com",
      "selector": "s1",
      "passed_domains": ["example.com"],
      "from_aligned": true
    },
    "spf": {
      "result": "pass",
      "domain": "example.com",
      "from_aligned": true
    },
    "dmarc": {
      "result": "pass",
      "from_domain": "example.com",
      "dkim_aligned": true,
      "spf_aligned": true
    }
  }
}

The legacy spf and dkim fields are available on all plans. Paid plans also receive auth_results, which includes the validated DKIM d= domain, selector, all DKIM domains that passed, and the computed DMARC alignment result for the MIME From domain.

Each request also carries metadata headers:

X-Postscale-Event: email.received
X-Postscale-Delivery-ID: <uuid>      (unique per attempt — use for dedup)
X-Postscale-Attempt: 1               (1-indexed; bumps on retry)
X-Postscale-Signature: t=…,v1=…      (verification — see below)

Verifying Webhooks

Every request signed by Postscale carries an X-Postscale-Signature header of the form t=<unix>,v1=<hex_hmac_sha256>. Verify it on every request to ensure the delivery came from Postscale and was not tampered with or replayed.

The official Node.js, Python, Ruby, and PHP SDKs include verifiers for this signature format. The full algorithm, t= / v1= details, dual-signature handling during a rotation grace window, and SDK examples live in the main Webhooks doc — that page is shared across inbound and outbound webhooks because the verification protocol is identical.

Threading

Use the In-Reply-To and References headers to match replies to original messages and maintain conversation threads.

Attachments

Each attachment exposes an authenticated Postscale download url. Fetch it with your normal API credentials, then copy the file into your own storage if your workflow needs retention outside Postscale's retention window. Where application-side attachment encryption is enabled, this API path also handles decryption after authorization.