Skip to content
Engineering

Signed Webhooks for Email: What to Verify First

| | 14 min read
Signed Webhooks for Email: What to Verify First
Signed Webhooks for Email: What to Verify First

When email arrives through a webhook, the first question should not be whether the sender looks legitimate, whether the subject matches, or whether the OTP regex works. The first question is simpler and more important: did this HTTP request really come from the service you trust to deliver the email?

That is what signed webhooks are for. A signed webhook lets your receiver verify the authenticity and integrity of an inbound event before the message enters your queue, test harness, or LLM agent workflow. For email automation, that order matters because inbound email is untrusted input and webhook endpoints are public HTTP surfaces.

Mailhook supports signed payloads for real-time webhook notifications, along with structured JSON emails, disposable inboxes via API, polling fallback, and custom domain support. For exact implementation details, use the canonical Mailhook llms.txt integration reference rather than guessing header names or signature formats.

The mistake: verifying the email before verifying the delivery

A common webhook handler starts by parsing JSON, extracting the sender, looking for a verification code, and then deciding what to do. That feels natural because the email content is the business object. It is also backwards.

Before you trust any parsed field, you need to authenticate the delivery envelope that carried it. Otherwise, an attacker can send a forged HTTP request to your webhook URL with a payload that looks like a real email event. If your automation accepts it, the attacker may be able to trigger false test passes, poison an agent memory store, submit a malicious magic link, or create noise in your CI pipeline.

The right sequence is:

  1. Preserve the raw HTTP body exactly as received.
  2. Read the signing headers from the request.
  3. Enforce timestamp freshness.
  4. Verify the signature using the webhook secret.
  5. Reject replays before processing.
  6. Parse and validate the structured email JSON.
  7. Extract only the minimal artifact your workflow needs.

This sequence is not specific to email. Providers such as GitHub document similar webhook validation principles, including using the raw payload and constant-time comparison in their guide to validating webhook deliveries. For HMAC-based schemes, the underlying construction is standardized in RFC 2104.

What to verify first, in priority order

Use this order as the front door of your email webhook receiver. The goal is to reject bad deliveries before the payload reaches your application logic.

Order Verification gate Why it comes first Fail behavior
1 Raw body capture Signature verification often depends on the exact bytes received Stop before parsing
2 Required signing metadata Missing, duplicated, or malformed headers make the request unverifiable Return 400 or 401
3 Timestamp freshness Limits the window for replayed requests Return 401
4 Signature match Proves the payload was signed with the shared secret Return 401
5 Replay detection Prevents a previously valid delivery from being processed again Return 2xx if already handled, otherwise reject
6 Idempotency key Makes retries and duplicate deliveries safe Upsert or no-op
7 Schema and correlation Ensures the email belongs to the expected inbox, test, or agent run Reject or quarantine

The first five gates are security gates. The last two are correctness gates. Mixing them together is where many brittle integrations begin.

Gate 1: capture the raw body before anything parses it

Most signature schemes sign the raw request body, or a canonical string that includes the raw body. If your framework parses JSON first, reorders fields, normalizes whitespace, changes encodings, or consumes the stream, your computed signature may not match the provider signature.

In Node, Python, Go, Ruby, and most serverless runtimes, this means you need an endpoint path that gives your handler access to the raw request bytes. Do not rely on a reconstructed JSON string. Do not verify against a pretty-printed body. Do not verify against selected fields.

The webhook receiver should treat raw body capture as a platform concern, not as business logic. In a production service, document this clearly so future middleware changes do not silently break verification.

Gate 2: require signing metadata, and reject ambiguity

A signed webhook usually includes metadata such as a timestamp, signature, algorithm version, and delivery identifier. The exact names and format are provider-specific, so follow your provider contract. For Mailhook integrations, check llms.txt for the current machine-readable reference.

At this stage, verify that required metadata exists, is well-formed, and is not ambiguous. Duplicate header values should be treated carefully because proxies and frameworks can merge headers in surprising ways. If the signature header appears twice or cannot be parsed unambiguously, fail closed.

This gate should not decide whether the email is useful. It only decides whether the request is verifiable.

Gate 3: enforce timestamp freshness before signature work becomes processing work

Timestamp tolerance protects you from replay attacks. A valid signed payload captured yesterday should not be accepted today. The receiver should compare the provider timestamp with the server clock and reject events outside a narrow tolerance.

A common tolerance is a few minutes, but the right value depends on your queueing model, provider retry behavior, and infrastructure latency. For CI and LLM-agent verification flows, short windows are usually preferable because inboxes are temporary and messages are expected soon after the triggering action.

Timestamp checks also help detect clock drift. If you suddenly see many valid-looking requests rejected for timestamp skew, monitor your server time synchronization before assuming the provider is broken.

Gate 4: compute the signature over the exact provider-specified input

Once you have the raw body and signing metadata, compute the expected signature according to the provider documentation. Many schemes use HMAC with SHA-256, but you should not assume the algorithm, canonicalization, or header names. The important rule is to verify exactly what the provider says it signed.

Use a constant-time comparison for the expected signature and the received signature. Ordinary string comparison can leak timing information in some contexts. Modern standard libraries usually provide a safe comparison function.

Also separate the webhook secret from API keys used to create inboxes or poll messages. A webhook signing secret has one job: verifying inbound event authenticity. It should be stored in a secret manager, rotated intentionally, and never logged.

Gate 5: detect replay before side effects

A replay is different from a duplicate. A duplicate can happen naturally because webhook delivery is often at-least-once. A replay is a previously valid delivery being submitted again, potentially by an attacker or by a broken intermediary.

Your receiver should store a replay key before doing side effects. Prefer a provider-attested delivery ID if the provider supplies one in signed metadata or in the verified payload. If no delivery ID exists, use a conservative fallback such as a hash of the verified raw body plus timestamp, depending on the documented provider semantics.

The replay store should have a TTL longer than the maximum accepted timestamp skew and expected retry window. For short-lived disposable inbox flows, this can often be small. For compliance-sensitive workflows, you may also keep a compact audit record with delivery IDs, timestamps, and status, without storing full email content longer than needed.

A reference webhook handler pattern

The following pseudocode is intentionally provider-neutral. Header names, canonical string construction, and signature format are placeholders. Use your provider documentation, and for Mailhook, use the llms.txt contract.

async function handleEmailWebhook(request) {
  const rawBody = await readRawRequestBody(request)

  const signing = extractSigningMetadata(request.headers)
  if (!signing.ok) {
    return response(400, 'missing or malformed signing metadata')
  }

  if (!isFresh(signing.timestamp, { toleranceSeconds: 300 })) {
    return response(401, 'stale webhook')
  }

  const signedInput = buildCanonicalInput({
    timestamp: signing.timestamp,
    rawBody
  })

  const expected = computeHmac({
    secret: WEBHOOK_SIGNING_SECRET,
    input: signedInput
  })

  if (!constantTimeEqual(expected, signing.signature)) {
    return response(401, 'invalid signature')
  }

  const replayKey = deriveReplayKey(signing, rawBody)
  const inserted = await replayStore.insertIfAbsent(replayKey, {
    ttlSeconds: 3600
  })

  if (!inserted) {
    return response(200, 'duplicate delivery ignored')
  }

  const event = JSON.parse(rawBody.toString('utf8'))
  const validation = validateEmailEventSchema(event)

  if (!validation.ok) {
    await quarantine(event, validation.reason)
    return response(202, 'quarantined')
  }

  await enqueueForProcessing({
    deliveryId: validation.deliveryId,
    inboxId: validation.inboxId,
    messageId: validation.messageId,
    event
  })

  return response(202, 'accepted')
}

Notice what is not happening in the verified path: no OTP extraction before signature verification, no link fetching inside the webhook handler, no LLM call during request authentication, and no business side effect before replay detection.

What to verify after the signature passes

A valid signature proves the webhook delivery came from the provider and that the signed payload was not modified in transit. It does not prove that every field inside the email is safe or true.

After authenticity is established, verify the email event as application data.

  • Schema: Required IDs, timestamps, recipient fields, and content fields should be present and type-safe.
  • Inbox correlation: The inbox ID or email address should match the run, attempt, test, tenant, or agent session that expected the message.
  • Message identity: Use stable message and delivery identifiers for dedupe. Do not dedupe only on subject text.
  • Artifact extraction: Extract the minimal OTP, verification URL, or routing token needed for the workflow.
  • Action authorization: A valid email event should not automatically authorize an arbitrary downstream action.

This is especially important in temporary email API workflows. The provider can deliver structured JSON emails, but your application still needs to decide whether a specific message belongs to a specific run. If you create one disposable inbox per attempt, correlation becomes much simpler because the inbox itself is a strong boundary.

For more on JSON-first email handling, see Mailhook’s guide to Email to JSON: A Minimal Schema for Agents and QA.

What not to verify first

Some checks are useful, but they belong later in the pipeline.

Do not start with the sender display name. Display names are sender-controlled and easy to spoof. Do not start with the subject line. Subject text is brittle and often changes when templates are edited. Do not start with HTML parsing. HTML can contain tracking pixels, misleading links, hidden text, or prompt-injection content. Do not start with DKIM or Authentication-Results fields from the email. They are useful signals, but they are email-level signals, not proof that the HTTP webhook request is authentic.

For webhook security, the HTTP delivery must be verified first. Then you can evaluate email authentication signals, sender claims, message content, and extracted artifacts according to your workflow.

Signed webhooks in LLM agent workflows

LLM agents make webhook ordering even more important. If an agent sees an unverified email payload, the content can influence tool calls, memory updates, or verification steps. A malicious email can include instructions such as ignore previous rules, click this link, or send this token elsewhere. Signature verification will not remove prompt injection from email content, but it ensures the content entered your system through an authenticated provider delivery.

A safer agent pipeline looks like this:

Stage Agent visibility Recommended behavior
Webhook verification None Verify signature, timestamp, replay key, and idempotency
Normalization None or limited logs Convert the email to structured JSON and validate schema
Artifact extraction Minimal Extract OTP, magic link, sender domain, or correlation token
Agent tool result Minimal Return only the typed artifact and safe metadata
Optional review Controlled Show raw email only to humans or sandboxed tooling

The agent does not need raw HTML to complete most verification flows. It usually needs a small typed result, such as an OTP, a verified magic link, or a not found status. Mailhook’s structured JSON output is useful here because it lets you build deterministic tools around email events instead of asking a model to reason over a whole mailbox.

For a deeper agent-specific pattern, see Structured Email for Agents: JSON Fields You Can Trust.

Operational details that prevent production incidents

The signature check is the centerpiece, but production reliability depends on the surrounding system.

Keep webhook handlers fast. Verify, dedupe, enqueue, and return a 2xx response. Do not perform slow browser automation, LLM reasoning, or third-party link fetching inside the request path. If processing fails after enqueueing, retry from your internal queue where you control backoff and observability.

Use polling as a reconciliation path, not as a substitute for verification. A webhook-first design gives low-latency delivery, while polling helps recover from missed notifications, deployment downtime, or queue outages. Mailhook supports both real-time webhooks and a polling API, which is useful for deterministic CI and agent workflows. The pattern is covered in Temp Email Receive: Webhook-First, Polling Fallback.

Rotate secrets carefully. During rotation, many teams support two valid secrets for a short overlap window: current and previous. Log which secret version verified the request, not the secret itself. Remove the old secret after all expected retries have expired.

Monitor verification failures as a security and reliability signal. Track missing signature headers, invalid signatures, stale timestamps, replay hits, schema failures, and quarantine counts. A sudden spike may indicate a broken deployment, a provider configuration change, a proxy modifying request bodies, or active probing of your webhook URL.

How Mailhook fits the pattern

Mailhook provides programmable disposable inboxes via API and delivers received emails as structured JSON, which makes email easier to consume in automation, QA, signup verification flows, and LLM-agent tools. For push-based ingestion, Mailhook supports real-time webhook notifications with signed payloads. For resilience, you can also use polling to reconcile messages when a webhook is delayed or your endpoint is temporarily unavailable.

A practical Mailhook-style flow is straightforward: create a disposable inbox via API, use the generated email address in the product or test flow, receive the email event through a signed webhook, verify the delivery before parsing, extract the minimal artifact from the structured JSON, then expire or stop using the inbox according to your lifecycle policy.

The exact API shapes and signature contract should come from the official Mailhook llms.txt reference. That is the safest source for developers and agents because it avoids relying on outdated examples or guessed header names.

Code review checklist for signed email webhooks

Use this checklist before shipping a webhook receiver for email automation.

  • The endpoint can access the exact raw request body.
  • JSON parsing happens only after signature verification succeeds.
  • Required signing metadata is validated and ambiguous duplicate headers fail closed.
  • Timestamp tolerance is enforced.
  • Signature comparison uses a constant-time function.
  • Webhook signing secrets are stored separately from API keys.
  • Replay detection happens before side effects.
  • Duplicate valid deliveries are idempotent.
  • Schema validation happens before artifact extraction.
  • Inbox ID, recipient, or correlation token is matched to the expected workflow.
  • LLM agents receive only a minimized view, not raw HTML or untrusted instructions.
  • Polling exists as a reconciliation path for missed webhook delivery.

Frequently Asked Questions

Should I parse JSON before verifying a signed webhook? No. Capture the raw body first and verify the signature over the provider-specified input. Parse JSON only after the signature, timestamp, and replay checks pass.

Does a valid webhook signature mean the email sender is trustworthy? Not necessarily. It means the webhook delivery came from your provider and the signed payload was not tampered with. Sender claims, links, attachments, and email authentication fields still need separate handling.

What should I do if the same signed webhook arrives twice? Treat duplicate delivery as normal. Use a replay or delivery key, make processing idempotent, and usually return a 2xx response for an already processed valid delivery so the provider stops retrying.

How should signed webhooks work with polling fallback? Webhooks should be the low-latency path. Polling should reconcile state when a webhook is missed, delayed, or your endpoint was unavailable. Polling results should still go through the same schema, dedupe, and correlation logic.

Where can I find Mailhook’s exact webhook signature details? Use the Mailhook llms.txt reference. It is the canonical integration reference for Mailhook’s API and agent-readable implementation details.

Build email automation on verified events

Signed webhooks are the security boundary between public HTTP traffic and your email automation pipeline. Verify the raw delivery first, reject stale or forged requests, prevent replay, then parse the email as structured data.

If you are building verification flows for AI agents, QA automation, or CI, Mailhook gives you disposable inboxes via API, structured JSON email output, real-time webhooks, signed payloads, and polling fallback. Start with the Mailhook integration reference and design your webhook receiver so only verified email events reach your agents and test runners.

Related Articles