- Create a transaction subscription for one or more wallets
- Handle and parse incoming webhook payloads
- Verify webhook signatures for security
- Manage (list, update, delete) your subscriptions
Prerequisites
- A Zerion API key (get one here)
- A publicly accessible callback URL to receive webhook notifications (you can use webhook.site for testing)
For production use, contact api@zerion.io to whitelist your callback URL. See Webhooks for plan limits and delivery guarantees.
Steps
Subscribe to transactions for one or more wallet addresses using the create subscription endpoint.
chain_ids is optional — omit it to subscribe to all supported chainsid from the response for management laterWhen a transaction occurs, Zerion sends a POST request to your callback URL. Here’s what the payload looks like and how to process it.
const express = require("express");
const crypto = require("crypto");
const app = express();
// Capture the exact raw body so signature verification uses the original bytes.
app.use(
express.json({
verify: (req, _res, buf) => {
req.rawBody = buf.toString("utf8");
},
})
);
// Cache fetched certificates to avoid re-downloading on every request
const certCache = new Map();
async function fetchCertificate(certUrl) {
if (certCache.has(certUrl)) return certCache.get(certUrl);
// Validate the certificate URL domain to prevent SSRF attacks
const url = new URL(certUrl);
if (!url.hostname.endsWith(".zerion.io")) {
throw new Error(`Untrusted certificate domain: ${url.hostname}`);
}
const res = await fetch(certUrl);
if (!res.ok) throw new Error(`Failed to fetch certificate: ${res.status}`);
const pem = await res.text();
certCache.set(certUrl, pem);
return pem;
}
async function verifyWebhook(req) {
const timestamp = req.headers["x-timestamp"];
const signature = req.headers["x-signature"];
const certUrl = req.headers["x-certificate-url"];
const pem = await fetchCertificate(certUrl);
const message = `${timestamp}\n${req.rawBody}\n`;
const verifier = crypto.createVerify("SHA256");
verifier.update(message);
return verifier.verify(pem, signature, "base64");
}
app.post("/webhook", async (req, res) => {
try {
if (!(await verifyWebhook(req))) {
return res.sendStatus(403);
}
const { data, included } = req.body;
// data contains the notification metadata
console.log("Notification ID:", data.id);
console.log("Wallet:", data.attributes.address);
console.log("Timestamp:", data.attributes.timestamp);
// included contains the full transaction object(s)
for (const tx of included) {
const { operation_type, transfers, fee, mined_at } = tx.attributes;
const chain = tx.relationships.chain.data.id;
console.log(`${operation_type} on ${chain} at ${mined_at}`);
for (const transfer of transfers) {
const symbol = transfer.fungible_info?.symbol || "NFT";
const direction = transfer.direction === "out" ? "Sent" : "Received";
console.log(` ${direction} ${transfer.quantity.float} ${symbol}`);
}
}
res.sendStatus(200);
} catch (error) {
console.error("Webhook processing error:", error);
res.sendStatus(400);
}
});
app.listen(3000);
Transaction prices in webhook notifications are always
null. To get USD values, fetch the full transaction using the wallet transactions endpoint after receiving the notification:x-timestamp, x-signature, and x-certificate-url`${timestamp}\n${rawBody}\n` — use the raw request body, not JSON.stringify(req.body)crypto.createVerify("SHA256") with the certificate’s public keyAlways validate the
x-certificate-url domain before fetching — ensure it points to a trusted Zerion domain to prevent SSRF attacks.List, update, or delete subscriptions as needed: list subscriptions, update wallets, or delete a subscription.
# List all subscriptions
curl -u "YOUR_API_KEY:" \
"https://api.zerion.io/v1/tx-subscriptions"
# Add or remove wallets from a subscription
curl -u "YOUR_API_KEY:" \
-X PATCH "https://api.zerion.io/v1/tx-subscriptions/SUBSCRIPTION_ID/wallets" \
-H "Content-Type: application/json" \
-d '{
"addresses": ["0xNEW_ADDRESS_TO_ADD"]
}'
# Delete a subscription
curl -u "YOUR_API_KEY:" \
-X DELETE "https://api.zerion.io/v1/tx-subscriptions/SUBSCRIPTION_ID"
Important notes
See Delivery guarantees and Subscription limits on the Webhooks reference page.Next steps
- Combine with the transaction history recipe to fetch full details (including prices) for flagged transactions
- Use
chain_idsto focus on specific chains and reduce noise - Build a notification service that forwards alerts to Slack, Discord, email, or push notifications