---
title: "Email webhook HMAC verification: a receiver checklist"
description: "Plan Mailera webhook verification around signing secrets, unchanged request bytes, timing-safe comparison, and negative tests that check for side effects."
url: https://mailera.app/en/blog/email-webhook-hmac-verification
published: 2026-10-25
updated: 2026-10-25
---

# Email webhook HMAC verification: a receiver checklist

Keep webhook management credentials separate from delivery-signing secrets. Put verification before event processing, then test that rejected requests cannot trigger downstream actions.

## What does HMAC verification check?

A webhook should not trigger an email action just because your endpoint received a plausible JSON object. Put signature verification before the code that queues work, calls an agent, or sends a reply.

HMAC is a message authentication mechanism that uses a cryptographic hash function and a shared secret. The receiver calculates an authentication value and compares it with the value supplied by the sender. [RFC 2104 defines the HMAC construction](https://www.rfc-editor.org/info/rfc2104/), including the secret’s role in calculation and verification.

Receiving a request does not mean you can trust it enough to process it. HMAC verification authenticates the signed input, not the instructions inside an email.

If you are designing the wider workflow, [email for AI agents using existing mailboxes](https://mailera.app/en/blog/email-for-ai-agents) covers the mailbox connection layer. This checklist focuses on the receiver’s authentication boundary.

## Keep the API key and signing secret separate

Mailera uses two credentials for different jobs:

| Credential | Job | Where to use it |
| --- | --- | --- |
| API key | Authorize REST API requests, including webhook management | The `Authorization: Bearer` header on requests to Mailera |
| Webhook signing secret | Verify incoming webhook deliveries | Protected configuration used by your receiver |

Create the API key in this order:

1. Open **Settings → API key tab → Create key**. Pick the mailbox scope this integration needs, then select the **Webhook** permission rather than unrelated ones.
2. Create the webhook through `POST /webhooks`, using the request format described in the [Mailera API documentation](https://mailera.app/en/docs). Mailera’s API base is `https://mailera.app/api/public/v1`.
3. Save the webhook signing secret immediately in protected configuration. Mailera returns it only once, when the webhook is created.
4. Use `message.received` for your test workflow, with a mailbox you own and synthetic email content.

The **Webhook** permission on an API key authorizes listing, creating, updating, and deleting webhooks. 

![Mailera API key creation dialog with the Webhook permission selected.](https://mailera.app/media/777eca42fb09a181/email-webhook-hmac-verification-a-receiver-checklist-en-1200.jpg)
_The Webhook permission authorizes management requests. Store the delivery-signing secret separately._

Do not hardcode either credential or commit it to a repository. [GitHub’s webhook validation guidance](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries) gives the same secret-storage advice, but its header names and signing format belong to GitHub, not Mailera.

For API integration guidance, see the [Mailera developer overview](https://mailera.app/en/developers). Authenticate webhooks in your receiver, before business logic.

## Treat header names and signing rules as different things

Mailera’s webhook header names are:

| Header | Purpose |
| --- | --- |
| `X-MailHub-Event` | Event header |
| `X-MailHub-Delivery` | Delivery header |
| `X-MailHub-Timestamp` | Timestamp header |
| `X-MailHub-Signature` | Signature header |
| `X-Mailera-Signature` | Signature header |

Knowing the header names doesn't tell you how to compute or compare the signature. Implement header selection, required values, and malformed-value handling from the sender’s signing contract. 

The same rule applies to the two signature header names. Do not implement an “accept whichever matches” fallback of your own. Handle both-present, conflicting, and duplicate-header cases according to the contract rather than whichever value your framework happens to return first.

Before choosing verification code, identify the algorithm, the secret’s byte representation, the exact signed input, and the signature’s encoding. Prefixes and version markers also belong to that format. Copying another provider’s HMAC example without these details can produce a verifier for the wrong protocol.

Preserve the incoming request bytes and headers before middleware changes them. GitHub’s own verification guidance states that payloads and headers must not be modified before verification. Use that as a receiver-design lesson, not as a substitute for Mailera’s signing rules.

## Put verification before every downstream action

Structure the receiver so that processing has one entry point, reached only after successful verification and event validation. Avoid starting work in middleware and checking the signature later.

![Receiver flow preserving request bytes, verifying headers and signatures, and rejecting invalid requests before downstream work.](https://mailera.app/media/0f88dae87af0ae26/email-webhook-hmac-verification-a-receiver-checklist-en-1200.jpg)
_Make rejection a terminal path. Only authenticated, validated events can reach downstream processing._

Use this implementation order:

1. **Capture the request with a size limit.** Preserve the original bytes. Choose the limit as an application policy, and reject oversized input before expensive processing.
2. **Validate the required headers.** Apply the signing contract’s rules for missing values, duplicates, and signature selection. Do not repair malformed input by guessing what the sender intended.
3. **Construct the signing input exactly.** Use the specified secret representation and signed byte sequence. Do not substitute parsed JSON or add a timestamp unless the contract requires it.
4. **Compute and compare the authentication value.** For HMAC-based protocols, Node.js exposes `crypto.createHmac(algorithm, key[, options])`; see the [Node.js Crypto reference](https://nodejs.org/api/crypto.html). Strictly decode the supplied signature, reject a length mismatch before comparison, and use a timing-safe comparison rather than plain equality.
5. **Parse and validate the event.** Only after authentication succeeds, check the event structure before passing it to a worker or durable queue.
6. **Stop on every failure path.** A missing secret, a decoding error, or a failed comparison must not fall through to processing.

GitHub’s verification guidance recommends methods such as `crypto.timingSafeEqual` instead of plain equality. Check the surrounding control flow too: a comparison function cannot protect a handler that already queued the event.

Define rejection responses as receiver policy. Keep those response choices separate from assumptions about the sender’s retries. The essential test is not the response text; it is whether rejected input can cause work.

## Test rejection, not just successful verification

Use a test mailbox you control. Keep its emails synthetic, and run signature checks locally rather than pasting secrets or email payloads into an online verifier.

Start with an authoritative sender test vector or a captured test delivery with its expected signature. A local signer and verifier can agree while sharing the same incorrect signing-input construction.

Use the following acceptance matrix.

| Test | Change | Required result |
| --- | --- | --- |
| Known-good delivery | Preserve the expected signed input and secret | Authentication succeeds; event validation runs |
| Changed signed input | Alter one byte without replacing the signature | Reject; no downstream action |
| Wrong secret | Verify the fixture with a different secret | Reject; no downstream action |
| Missing signature | Remove the required signature input | Reject; no downstream action |
| Malformed signature | Supply a value outside the required encoding | Reject without falling through |
| Wrong signature length | Supply a decoded value of the wrong length | Reject before timing-safe comparison |
| Header conflict | Exercise both-present and disagreement cases | Follow the documented selection or rejection rule |
| Duplicate header | Send repeated signature-header values | Follow the documented duplicate-header rule |

Also exercise Unicode, whitespace, and line endings. Make changes to bytes that belong to the signed input, rather than assuming every request component is signed. Preserve the original fixture so each mutation is reproducible.

For each rejection test, assert that the worker was not called, no job was enqueued, and no email action occurred. An error response alone is not enough: a handler could return an error after starting work.

Keep cryptographic primitive tests separate from delivery-format tests. [RFC 4231 supplies HMAC-SHA test vectors](https://www.rfc-editor.org/info/rfc4231/), including HMAC-SHA-256. Those vectors test the HMAC calculation; use sender-specific fixtures to test the delivery format as well.

## Why does webhook signature verification fail?

When verification fails, inspect the input boundary before changing cryptographic code.

| Symptom | Possible cause | Check |
| --- | --- | --- |
| Verification fails after adding JSON middleware | The verifier receives reconstructed JSON rather than the original input | Capture bytes before parsing or reserialization |
| Every delivery fails | The receiver uses the API key instead of the signing secret | Check the configuration reference without printing either credential |
| Calculated and supplied values have different lengths | Encoding or prefix handling differs from the contract | Validate the expected format before decoding |
| The verifier sees an empty body | Earlier middleware consumed the request stream | Inspect middleware order and recorded request size |
| Behavior changes when both signature headers appear | Header selection is implicit or inconsistent | Test selection, disagreement, and duplicate handling explicitly |
| Only some text payloads fail | Text transformations affect signed bytes | Compare synthetic fixtures containing Unicode and different line endings |

Treat byte preservation as a requirement even when the parsed JSON looks right. Do not parse and reserialize a request to make its formatting easier to inspect before verification.

For production diagnostics, record the verification outcome, request size, and a sanitized correlation identifier. Keep the signing secret, full signatures, and email body out of logs. Debug payload transformations with synthetic fixtures instead of credential or payload dumps.

## Keep replay checks and agent safety separate

Do not treat HMAC verification as replay prevention, event deduplication, or exactly-once processing. Design those controls separately from the signature comparison.

For a freshness check, use authenticated time information with defined timestamp semantics. Do not trust a timestamp simply because it is present in `X-MailHub-Timestamp`. Choose any acceptance tolerance as an application policy, with clock skew in mind, rather than presenting it as a provider guarantee.

Keep durable duplicate handling outside the signature verifier. Authentication asks whether to accept the signed input; duplicate handling asks whether the application has already acted on the event. For the surrounding application design, see [reliable email-agent API workflows](https://mailera.app/en/blog/reliable-email-api-agent-workflows).

After those checks, the email itself remains untrusted data. A message can contain prompt injection even when its webhook delivery passes authentication. Never let an agent treat instructions embedded in an email as trusted commands solely because verification succeeded.

Give agents only the permissions they need, and require confirmation before sending. The [guide to connecting Claude or ChatGPT to Gmail safely](https://mailera.app/en/blog/is-it-safe-to-connect-claude-to-gmail) covers the wider agent-access question; [Mailera’s security page](https://mailera.app/en/security) is the product security reference.

Mailera does not guarantee inbox placement. A correctly authenticated webhook also doesn't change the provider's filtering decision or the sender's SPF/DKIM/DMARC configuration; those are separate controls from webhook verification.

**Do this with the API** — Every action in this article is available via the REST API, with curl examples, API keys and webhooks. [View API docs](https://mailera.app/en/docs)
