Verify webhook signatures
Confirm that a webhook request came from MailGraf before processing its payload.
Every MailGraf webhook delivery includes an HMAC signature. Verify it before trusting the payload so a third party cannot send a forged event to the same endpoint.
Read the signing values
Open the endpoint under Settings > Webhooks. Use Reveal beside Signing secret, copy the value beginning with whsec_ and store it in the receiving service's secret manager. Do not put the secret in source code or logs.
Each request carries this header:
X-MailGraf-Signature: t=<unix_timestamp>,v1=<hex_signature>
MailGraf signs the exact request body with HMAC-SHA256. The signed message is:
<timestamp>.<raw_request_body>
Verify before parsing JSON
Capture the raw body
Read the request body as bytes before a JSON parser or middleware changes it.Read t and v1
Split the X-MailGraf-Signature header into its timestamp and hexadecimal signature.Calculate the expected signature
Join the timestamp, a full stop and the raw body. Sign that value with the endpoint's signing secret and HMAC-SHA256.Compare in constant time
Compare the expected and received signatures with a timing-safe function. Process the event only when they match.
Node.js
Pass the unmodified body as a Buffer:
const crypto = require("crypto");
function verifyMailGrafSignature(secret, signatureHeader, rawBody) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.trim().split("=", 2))
);
if (!parts.t || !parts.v1) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.`)
.update(rawBody)
.digest();
const received = Buffer.from(parts.v1, "hex");
return (
received.length === expected.length &&
crypto.timingSafeEqual(received, expected)
);
}
Python
Pass the unmodified body as bytes:
import hashlib
import hmac
def verify_mailgraf_signature(secret, signature_header, raw_body):
parts = dict(
part.strip().split("=", 1) for part in signature_header.split(",")
)
timestamp = parts.get("t")
received = parts.get("v1")
if not timestamp or not received:
return False
message = timestamp.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)
After verification, queue any slow work and return a 2xx response within seven seconds. MailGraf treats a slower response as a failed delivery even if the service finishes processing later.
Rotate the signing secret
Use Rotate when the secret may have been exposed or as part of your own security schedule. Rotation invalidates the old secret immediately. Copy the new value, update the receiving service and click Send test event to confirm the change.
Reject requests that do not match
Do not process an event when the header is missing, malformed or does not match the calculated signature. Never fall back to accepting an unsigned payload.
Where the full reference lives
The API reference describes MailGraf's public API. Webhook signing values and verification examples are available on each endpoint under Settings > Webhooks.
Frequently asked questions
Can I parse the JSON before verification?
No. Verify the signature against the exact raw request body first. Parsing and serialising the JSON can change the bytes and invalidate the signature.
What happens when I rotate the signing secret?
The old secret stops working immediately. Update the receiving service with the new secret and send a test event.
What should I do when verification fails?
Do not process the payload. Check the endpoint's current signing secret, the signature header and how the server captures the raw body.
Related articles
Was this helpful?
Still need help?
Write to us and a person will answer.

