How to Authenticate Your Webhook Endpoint
When email-webhook fires your endpoint, the request comes from the internet with no built-in signature. This guide explains the two layers of protection available and how to validate them in your receiving server.
Layer 1: Keep your email address secret
Your incoming address — abc123xyz@email-webhook.com — is the first line of
defence. The local part is a system-generated opaque ID, not a guessable name.
Anyone who knows it can trigger your webhooks, so treat it like a password:
don't commit it to public repos, don't paste it in public docs.
If you suspect the address has leaked, delete your webhooks and create new ones to get a fresh address.
Layer 2: Custom authentication headers
The second layer is a secret header that your endpoint checks on every request. You add it once in the dashboard; email-webhook sends it with every delivery.
In the webhook form, open the Custom Headers section and add a key-value pair:
| Header name | Header value |
|---|---|
Authorization |
Bearer your-secret-token |
Or equivalently with an API key header:
| Header name | Header value |
|---|---|
X-Api-Key |
your-secret-token |
You can add up to 5 custom headers per webhook.
Validating the header in your server
Node.js (Express)
app.post("/webhook", (req, res) => {
if (req.headers["authorization"] !== "Bearer your-secret-token") {
return res.sendStatus(401);
}
const { from, subject, message } = req.body;
// handle email...
res.sendStatus(200);
});
Python (Flask)
@app.post("/webhook")
def handle_email():
if request.headers.get("Authorization") != "Bearer your-secret-token":
return "", 401
data = request.get_json()
# handle email...
return "", 200
Ruby (Sinatra)
post "/webhook" do
halt 401 unless request.env["HTTP_AUTHORIZATION"] == "Bearer your-secret-token"
data = JSON.parse(request.body.read)
# handle email...
status 200
end
Return 401 (or any non-2xx status) and email-webhook records the delivery as
failed. The request is not retried if the status code is 4xx.
On bearer tokens
Generally, prefer an API key over a bearer token. If you do need a bearer token,
make sure it has a long enough duration (usually encoded as the exp parameter)
so that your webhook requests succeed over time. You can learn more about bearer
token at https://jwt.io
Using `X-email-webhook-id` as an idempotency key
Every delivery includes an X-email-webhook-id header containing a UUID unique
to that request. Store it on the records you create and reject duplicates — this
protects you if your server returns a non-2xx status but the processing already
completed.
app.post("/webhook", async (req, res) => {
if (req.headers["authorization"] !== "Bearer your-secret-token") {
return res.sendStatus(401);
}
const deliveryId = req.headers["x-email-webhook-id"];
if (await alreadyProcessed(deliveryId)) {
return res.sendStatus(200); // acknowledge without re-processing
}
// handle email...
res.sendStatus(200);
});