SolidHooksSolidHooks
Get Started
Use casesIngest webhooks via email

Turn inbound email into a webhook

Plenty of systems will never send you a webhook. A supplier emails a notice, a bank emails an alert, an internal tool emails a nightly report. Give any of them a SolidHooks address and what lands there becomes a stored event with a parsed envelope, routed through the same connections and retried on the same schedule as HTTP traffic.
One address per sourceDedupe on Message-IDSame retries and receipts
What this guide covers
  1. 01Creating an email source and finding its address
  2. 02The envelope your destination receives
  3. 03Routing on sender or subject
  4. 04Dedupe, size budget and attachments
  5. 05The one rule: a source does email or HTTP, never both

How it fits the pipeline

Email is a provider on a source, picked the same way you pick GitHub or Stripe, not a separate product. Once the mail is parsed it is an event like any other, which means the parts of SolidHooks you already understand keep working: connections match it, deliveries retry it, retention expires it, and your consumer receives JSON over HTTP as usual.

Sender
any mail client
Source address
<maskId>@sdhk.events
Stored event
parsed envelope, typed
Destination
HTTP POST
Your consumer never speaks SMTP. It receives the same JSON POST it would get from a webhook provider.

Set it up

  1. 1

    Create a source with the email provider

    Pass provider: "email" at create time, or PATCH an existing source onto it. The response carries emailAddress, which is null on every other provider.

    curl
    curl -s "$API/v1/sources" \  -H "authorization: Bearer $SDHK_KEY" \  -H 'content-type: application/json' \  -d '{ "name": "Supplier notices", "provider": "email",       "verifierConfig": {} }' → { "id": "src_...", "maskId": "Kb3xk29ZtQm7Rv1A",    "emailAddress": "Kb3xk29ZtQm7Rv1A@sdhk.events", ... }

    Converting a source that already has a verifier needs verifierType: "noop" in the same PATCH. Nothing signs an inbound email, so the API refuses to carry the stored credential across rather than keeping a secret it would never read. The same PATCH also clears the source's eventTypeField and any idempotencyKeys, both of which described a payload shape that no longer arrives.

  2. 2

    Hand the address to the sender

    Paste it wherever the other system expects a recipient. There is no verification step and no DNS to configure on your side.

  3. 3

    Connect the source to a destination

    Same as any other source. What you filter on is the sender or the subject, whichever the source's eventTypeField names, or email.received if you left that unset. Routing on sender or subject is covered below.

What your destination receives

The stored payload is a normalised envelope rather than a raw MIME blob, so your handler does not need a mail parser. Addresses are split out, both bodies are separated, and attachments arrive as base64 with their metadata.

payload
{  "from": "billing@supplier.example",  "to": ["Kb3xk29ZtQm7Rv1A@sdhk.events"],  "cc": [],  "subject": "Invoice #42",  "messageId": "<abc123@mail.supplier.example>",  "date": "Tue, 14 Jul 2026 09:00:00 +0000",  "text": "plain-text body",  "html": "<p>html body</p>",  "spamStatus": "NotSpam",  "bounce": false,  "attachments": [    { "name": "invoice.pdf", "contentType": "application/pdf", "size": 12345, "data": "<base64>" }  ]}

from is the message's From: header reduced to a bare address, so Acme Billing <billing@supplier.example> arrives as billing@supplier.example. That is what makes it usable as a filter key. The display name drifts, and the Return-Path underneath is a per-message bounce address on anything sent through a mail service. That envelope sender is used only as a last resort, when a message carries no From: header at all.

The event's raw body keeps the full original payload from the mail provider, including any fields not listed above, with attachments[].data stripped to null. Attachment bytes live in the envelope only, which keeps one large PDF from being stored twice.

A source does one or the other

On an email source, an HTTP POST to that source's ingest URL returns 400 BAD_REQUEST with the message that the source ingests email. This is deliberate. Mail and webhooks arrive on different paths and only one of them produces an envelope, so a source that accepted both would hand your consumer two payload shapes under one event type.

Consequence for idempotency
idempotencyKeys is rejected with 400 on an email source. Dedupe there is the Message-ID, which is not a path you get to choose.

Routing on sender or subject

An email's event type comes from the source's eventTypeField, the same knob an HTTP source uses. Point it at body.from and each message is typed with the sender address, which a connection filter then matches like any other event type. Matching ignores case, because mail systems do not preserve the case of a From: header, so an allowlist written once keeps matching a sender that later arrives as Billing@Supplier.example. Replaying routes on the type persisted at ingest, so filters behave identically the second time.

Two paths are available and no others: body.from and body.subject. They are the fields that actually separate one message from another. The dashboard offers the same two, so a source configured through the API stays editable in the SolidHooks Dashboard UI.

curl
# Type each email with its sender instead of email.receivedcurl -s -X PATCH "$API/v1/sources/src_..." \  -H "authorization: Bearer $SDHK_KEY" \  -H 'content-type: application/json' \  -d '{ "data": { "eventTypeField": "body.from" } }' # Then a connection takes only that supplier:#   "filterRules": { "eventTypes": ["billing@supplier.example"] }
Leave it unset and you get one type
With no eventTypeField, every accepted email is typed email.received. It is still filterable, just not separable by sender. The same fallback applies per message: an email with no subject on a body.subject source lands on email.received rather than dropping out of every filter.

Dedupe and the size budget

Mail is larger and messier than webhook JSON, so the rules about what survives are worth knowing before a 20MB attachment arrives at 3am. Nothing is rejected for being too big. It is trimmed to fit, and the budget you are fitting into tops out at 4,096 KB.

dedupeOn Message-ID. A repeat returns duplicate: true and stores nothing.
budgetThe source's maxPayloadSizeKb, shared by bodies and attachments. Between 1 and 4,096 KB, defaulting to 256.
text firstPlain text is charged first and sliced if it alone busts the budget.
html secondDropped whole to null if text plus html would still bust it, never partially cut. Either case sets bodyTruncated: true.
attachmentsMetadata is kept, data becomes null and truncated: true is set. Bytes are never partially cut. What is charged is the base64, so budget for roughly a third more than the file size.
rotationrotate-mask changes the address, because it is derived from the maskId.

Practices worth adopting

  • Set eventTypeField to body.from before you need it. Filters written against email.received have to be rewritten once a second sender shows up at the same address.
  • Raise maxPayloadSizeKb before you need it if the sender attaches PDFs. The ceiling is 4,096 KB, and base64 costs about a third more than the file itself. Discovering the budget through a truncated flag is a worse way to learn it.
  • Read spamStatus and bounce in your handler. They are in the envelope because mail arrives from senders you do not control.
  • Treat the address as a credential. Anyone who has it can create events on that source, so rotate it if it ends up somewhere public.

Questions

Give the systems that only email you a webhook

Pick email as the provider, hand out the address, and treat the result like any other event.