How to build a support inbox with an inbound email API

Published on March 21, 2026

Step-by-step guide to turning a customer-facing inbox into structured webhooks. Parse MIME, handle attachments, detect replies, and route by subject pattern.

Every product eventually needs to accept incoming email — customer support, reply-to-open-ticket, bounce processing, forwarded user feedback. The DIY path is a rabbit hole: you have to run an MTA, parse MIME, handle attachments, deal with TLS, and maintain it forever. An inbound email API skips the whole thing.

Here's how to wire one up in under an hour.

What you're building

A pipeline that takes incoming email for support@yourapp.com and delivers it to your application as a structured webhook:

Customer hits "Reply" in Gmail
      │
      ▼
MX record → mx.postscale.io
      │
      ▼
Postscale parses MIME, extracts attachments, verifies DKIM
      │
      ▼
Webhook POST → https://yourapp.com/api/email/inbound
      │
      ▼
Your handler creates a ticket, posts to Slack, updates the DB — whatever you want

The part that used to require Postfix, opendkim, amavisd, spam filtering, and a full-time sysadmin now becomes a single HTTP handler.

Step 1 — Point your MX record

Whatever DNS provider you use:

yourapp.com.  MX  10  mx.postscale.io.

That's it. mx.postscale.io already has the valid TLS cert, rate limits, and spam filtering. Propagation takes 5–60 minutes depending on your zone's TTL.

Unfamiliar with MX records? DNScale has a primer on MX records covering priority, failover, and what 10 in the example above actually means.

Step 2 — Verify ownership

Add one TXT record so Postscale knows you own the domain (TXT record format reference if your DNS provider has unusual quoting rules):

_postscale.yourapp.com.  TXT  "postscale-verify=abc123..."

Then call:

curl -X POST https://api.postscale.io/v1/domains \
  -H "Authorization: Bearer ps_live_..." \
  -d '{"domain": "yourapp.com", "type": "inbound"}'

Status goes from pending to verified as soon as DNS resolves.

Step 3 — Register a webhook

Tell Postscale where to POST incoming messages:

curl -X POST https://api.postscale.io/v1/webhooks \
  -H "Authorization: Bearer ps_live_..." \
  -d '{
    "url": "https://yourapp.com/api/email/inbound",
    "events": ["email.received"],
    "signing_secret": "generate-a-random-32-byte-string"
  }'

Step 4 — Receive a webhook

Here's what lands on your endpoint:

{
  "id": "in_01HY2K4...",
  "event": "email.received",
  "created_at": "2026-04-16T11:22:00Z",
  "message": {
    "message_id": "<CABcDeF...@mail.gmail.com>",
    "from": { "email": "customer@example.com", "name": "Jane Doe" },
    "to": [{ "email": "support@yourapp.com" }],
    "subject": "Password reset not working",
    "in_reply_to": null,
    "references": [],
    "text_body": "Hi, I tried to reset my password but...",
    "html_body": "<p>Hi, I tried to reset my password but...</p>",
    "attachments": [
      {
        "filename": "screenshot.png",
        "content_type": "image/png",
        "size_bytes": 128432,
        "url": "https://storage.postscale.io/att/abc?sig=..."
      }
    ],
    "spf": "pass",
    "dkim": "pass"
  }
}

Note:

  • Attachments over 1 MB aren't inlined. You get a pre-signed URL valid for 24 hours.
  • Both text_body and html_body are present when the sender sends multipart/alternative. Use text_body for indexing; render html_body for display.
  • spf and dkim results are pre-checked. Reject anything where both fail.

Step 5 — Verify the signature

Every webhook carries an HMAC-SHA256 signature in the X-Postscale-Signature header. Verify in Go:

func verifySignature(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Reject anything that doesn't verify. Without this, anyone can forge inbound emails by POSTing to your endpoint.

Step 6 — Thread replies

When someone replies to an email you sent, the customer's mail client sets In-Reply-To: and References: headers pointing at your original message. Use them to attach the reply to the right ticket:

ticket := findTicketByMessageID(msg.InReplyTo)
if ticket == nil && len(msg.References) > 0 {
    ticket = findTicketByMessageID(msg.References[0])
}
if ticket == nil {
    ticket = createNewTicket(msg)
}
ticket.AddReply(msg)

Don't rely on Re: ... subject parsing — it breaks on non-English mail clients (AW: in German, VS: in Danish, 回复: in Chinese).

Step 7 — Route by address

Most teams want more than one inbox: support@, billing@, security@. Two approaches:

Subaddressing (RFC 5233): send every address to one endpoint and inspect the recipient:

support+ticket-4421@yourapp.com  →  your endpoint
billing@yourapp.com              →  same endpoint
security@yourapp.com             →  same endpoint

Your handler reads message.to[0].email and routes accordingly. Simple, no config.

Catch-all with rules: set a rule per pattern in the Postscale dashboard. Rules match on to_local_part, from_domain, subject_regex, and dispatch to different webhook URLs. Good when different teams own different inboxes.

Step 8 — Handle the edge cases

The boring ones that break production:

  • Empty bodies. Some auto-responders send no body. Handle text_body == "" without a null-pointer.
  • Huge threads. Someone replies to a 50-person thread and the message is 2 MB of quoted history. Strip quoted content before indexing — or just look at the top block before the first >.
  • Out-of-office autoreplies. Detect auto-submitted: auto-replied header or subjects starting with Auto: , Automatic reply:. Don't create a ticket from these.
  • Loops. If your system replies to inbound mail, always set Auto-Submitted: auto-generated and Precedence: bulk so receiving systems don't reply back.
  • Spam. Postscale pre-filters obvious spam but anything with DKIM/SPF pass and a low-reputation sender still gets through. Add a second-layer filter on words/patterns if support-inbox volume warrants.

Deployment checklist

Before you go live:

  • MX points at mx.postscale.io
  • Domain verified via TXT
  • Webhook endpoint returns 200 within 15 seconds (Postscale retries on 4xx/5xx)
  • HMAC signature verified on every request
  • Auto-reply suppression (Auto-Submitted: auto-generated on your outbound)
  • Reply threading via In-Reply-To, not subject regex
  • Attachment URLs stored long-term in your own S3 if you need > 24h retention
  • Metrics on webhook 2xx rate — alert if it drops below 99%

Further reading

If you're evaluating vendors, our Inbound Email API page covers the product features and free-tier limits.