← Back to docs

Inbound Webhooks

Inbound 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.

Setup

  1. Configure your domain's MX records to point to Postscale.
  2. 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).
  3. 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 you only send outbound mail through Postscale, use an outbound domain instead and leave your mailbox provider's MX records unchanged.

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"
}

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 full algorithm, t= / v1= format details, dual-signature handling during a rotation grace window, and Node + Python reference verifiers 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.