Test Alchemy webhooks
Alchemy Notify sends a POST to your endpoint when something happens on chain, such as activity on an address you watch. This guide shows what arrives, which headers matter, and how to check the signature before you trust the body.
Point your Alchemy webhook at it and see a real event.
What Alchemy sends
Each event is one POST with a JSON body. An Address Activity event, trimmed to one transfer and with placeholder values, looks like this:
{ "webhookId": "wh_0000000000000000", "id": "whevt_0000000000000000", "createdAt": "2026-10-09T12:00:00.000000000Z", "type": "ADDRESS_ACTIVITY", "event": { "network": "ETH_MAINNET", "activity": [ { "blockNum": "0x0", "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", "fromAddress": "0x0000000000000000000000000000000000000001", "toAddress": "0x0000000000000000000000000000000000000002", "value": 10, "asset": "USDC", "category": "token", "rawContract": { "rawValue": "0x0000000000000000000000000000000000000000000000000000000000989680", "address": "0x0000000000000000000000000000000000000003", "decimals": 6 } } ] }}
id identifies the event and webhookId the webhook it was sent for, so use id to drop duplicates when Alchemy retries. blockNum and rawValue are hex strings.
Headers
| Header | Example value | Meaning |
|---|---|---|
| Content-Type | application/json | The body is JSON. Keep the raw bytes for the signature check before you parse it. |
| X-Alchemy-Signature | 3df63ab6b8e2a59a…71edaf | HMAC-SHA256 of the raw body, keyed with this webhook's signing key, as hex. Nothing else is in the header. |
Alchemy delivers from two fixed addresses, 54.236.136.17 and 34.237.24.169, if you want to allowlist them as well.
Verify the signature
Compute an HMAC-SHA256 over the raw request body with the webhook's signing key, hex-encode it, and compare it with X-Alchemy-Signature in constant time.
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
// validAlchemySignature reports whether signature is the hex HMAC-SHA256
// of body. Pass the raw request bytes, not re-encoded JSON.
func validAlchemySignature(body []byte, signature, signingKey string) bool {
mac := hmac.New(sha256.New, []byte(signingKey))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
import { createHmac, timingSafeEqual } from "node:crypto";
// body must be the raw request bytes (a Buffer from express.raw), not parsed JSON.
export function validAlchemySignature(body, signature, signingKey) {
const expected = Buffer.from(createHmac("sha256", signingKey).update(body).digest("hex"));
const received = Buffer.from(signature ?? "");
return expected.length === received.length && timingSafeEqual(expected, received);
}
Respond quickly
Return 200 as soon as the event is stored, then do the slow work. Any other status makes Alchemy retry with exponential backoff, for up to 10 minutes on Free and pay-as-you-go plans.
Common mistakes
- Parsing the JSON and serialising it again before hashing. Alchemy's docs say the signature covers the raw string body; key order and whitespace change otherwise.
- Using your app's API key instead of the webhook's signing key. Each webhook has its own key in the Notify dashboard.
- Comparing signatures with
==. Usehmac.Equalin Go ortimingSafeEqualin Node.js. - Not deduplicating on
id, so a retried event is processed twice.
Try it with a real event
Create a test URL, paste it into the Alchemy dashboard as the webhook URL, and trigger some activity. To check a captured signature by hand, use the signature verifier with the Alchemy preset and the body from the inspector's Raw view.