Unitpost
Inbound

Receive email, not just send it.

Point MX at Unitpost. Any address on your domain becomes a webhook.

How it works

Sending, in reverse.

Mail lands, we parse it, then POST a signed webhook.

01

Point one MX record

One MX on a domain you already send from. Same auth.

02

We parse the MIME

Headers, text, HTML, attachments. Catch-all on the domain.

03

You get one signed event

email.received. Same signing, retries, and log.

One event, one handler

It's just another webhook.

email.received — same signed envelope as every other webhook.

POST https://acme.com/api/webhooksEnabled
Listening foremail.*Attemptsauto-retry ×5Signing keywhsec_••••••••
  • email.delivered
    1s200
  • email.opened
    2h200
  • email.bounced
    5h429
  • email.clicked
    8h200
acme.comVerifying
DKIMSigns your mail. Proves the domain is yours.Pending
SPFAuthorizes our servers to send for you.Pending
DMARCBetter inbox placement. Stops spoofing.Pending

Safe by default

Scanned, verified, never in your payload.

Scanned. Verdicts on the event. Bytes stay out of the payload.

  • Spam & virus

    Confirmed viruses rejected at ingest.

  • Attachments

    Metadata on the event. Bytes via signed URLs.

  • SPF · DKIM · DMARC

    Authentication results attached.

Message scan
  • Spam

    Flagged, never silently dropped

    spam_verdict
  • Virus

    Confirmed viruses rejected at ingest

    virus_verdict
  • SPF

    Sender-policy alignment

    spf_verdict
  • DKIM

    Signature validation

    dkim_verdict
  • DMARC

    Policy evaluation

    dmarc_verdict

Developer-first

Fetch the full message on demand.

Same REST API and keys you send with.

  • Parsed

    Headers, text, HTML, attachment metadata.

  • Signed downloads

    URLs expire. No leaked links.

Read a received email
import { Unitpost } from "unitpost";

const unitpost = new Unitpost(process.env.UNITPOST_API_KEY);

// The webhook carries metadata — fetch the parsed message on demand.
const email = await unitpost.email.received.get("received_email_9f2c…");
console.log(email.subject, email.text, email.spam_verdict);

// Attachments come back as signed, expiring download URLs.
const file = await unitpost.email.received.attachment(
  email.id,
  email.attachments[0].id,
);
console.log(file.url); // expires in 5 minutes

What you can build

Turn inbound mail into product

Every message is a structured event. The inbox is an integration point.

Support inboxes

support@ becomes events. Tickets open in your helpdesk.

Reply handling

Users reply by email. Threads land back in your app.

Parse & extract

Forwarded receipts, orders, forms. Pull the data out.

Email-to-action

Create a task, log an expense, kick off a build.

Attachment intake

Collect files by email. Fetch them over signed URLs.

Conversation history

Full thread next to what you sent. Per contact.

Included free

Send and receive on your domain

$0/ mo

  • Included on every plan, including free
  • One MX record — catch-all for your domain
  • Signed email.received webhooks
  • Reuses your verified domain auth
  • Parsed headers, bodies & attachments
  • Same REST API, dashboard & SDKs

Questions

Common questions

How do I receive email on my own domain with Unitpost?

Turn on receiving for a domain you already send from, add the one MX record we show you, and subscribe a webhook endpoint to the email.received event. Once the MX verifies, every message addressed to that domain is parsed and POSTed to your endpoint. There's no new domain and no separate account — receiving reuses the domain authentication you already verified for sending.

Does the inbound MX record break my existing email?

It can, which is why we check first. Our MX must be the lowest-priority value present or mail won't route to us — so if your root domain already has MX records for Google Workspace or Microsoft 365, pointing it at us would stop mail reaching those inboxes. We detect the conflict, warn you before you proceed, and recommend using a subdomain like inbound.yourdomain instead.

Which addresses receive mail — do I configure each one?

No per-address setup. Receiving is catch-all: any address at the enabled domain lands, and you route on the to field in your own handler. So support@, billing@, and reply+abc123@ all arrive without provisioning anything, which is what makes per-user or per-thread reply addresses practical.

How do I get the message body and attachments?

The email.received webhook carries metadata only — sender, recipients, subject, message_id, and attachment descriptors — so payloads stay small. Fetch the parsed message with GET /api/v1/email/received/{id}, which inlines text and HTML when they're small, then call GET /api/v1/email/received/{id}/attachments/{attachmentId} for a signed download URL that expires in about five minutes. Both need the emails:read scope.

Are inbound messages checked for spam and viruses?

Yes. Every message carries spam, virus, SPF, DKIM, and DMARC verdicts on the event and the stored record, so your handler decides how much to trust a sender. Confirmed viruses are rejected at ingest rather than handed to you. Messages over 25 MB in total, including attachments, are also rejected at ingest and the sender gets a failure notice.

Can I reply in-thread to an email I received?

Yes. Pass the received email's id as in_reply_to on POST /api/v1/email and we populate the In-Reply-To and References headers from the stored Message-ID chain, and prefix the subject with Re: if it isn't already — so mail clients thread the conversation. Any In-Reply-To or References header you set explicitly still wins.

Is inbound email included on the free plan?

Yes. Receiving is included on every plan, including free — it is not a paid add-on. You still need a verified domain and a webhook subscribed to email.received. Received messages don't draw the paid sending allowance; on the free plan they share the monthly email quota with outbound sends.