Test Moralis Streams webhooks
Moralis Streams watches EVM addresses and contracts and posts matching activity to your webhook URL. Before it delivers anything real, it checks the URL: a stream does not start until its test webhook gets a 2xx. A temporary URL answers 200 to everything, so you can create the stream, see the test request and the deliveries that follow, and only then point it at your own server.
Point your Moralis Streams webhook at it and see a real event.
What Moralis sends
Moralis sends a test webhook whenever you create a stream, update one, or reactivate a paused one. Only the status code matters. For an EVM stream it looks like this:
{ "confirmed": true, "chainId": "", "streamId": "", "tag": "", "retries": 0, "block": { "number": "", "hash": "", "timestamp": "" }, "logs": [], "txs": [], "txsInternal": [], "erc20Transfers": [], "erc20Approvals": [], "nftTransfers": [], "nftApprovals": { "ERC721": [], "ERC1155": [] }, "abi": {}}
Empty arrays and an empty streamId mark it as the test: reply 200 and don't store it. Real deliveries have the same shape, filled in, and each event normally arrives twice: first with "confirmed": false as soon as the transaction is in a block, which can still be reorganised away, then with "confirmed": true once the block is final enough. Occasionally the confirmed one arrives first.
Headers
| Header | Example value | Meaning |
|---|---|---|
| x-signature | 0x9c1d…4be0 | Keccak-256 of the body with your Streams secret appended, as 0x-prefixed hex. |
Verify the signature
The signature is not an HMAC. Moralis computes it with web3.utils.sha3, which is Keccak-256, over the body with the Streams secret appended. The secret is a Streams-specific value you can read from the Streams settings endpoint; it is not your Moralis API key.
import (
"crypto/subtle"
"encoding/hex"
"golang.org/x/crypto/sha3"
)
// validMoralisSignature uses legacy Keccak-256, as Ethereum does. It is not
// sha3.New256, the standardised SHA3-256, which gives different output.
func validMoralisSignature(body []byte, signature, secret string) bool {
h := sha3.NewLegacyKeccak256()
h.Write(body)
h.Write([]byte(secret))
expected := "0x" + hex.EncodeToString(h.Sum(nil))
return subtle.ConstantTimeCompare([]byte(expected), []byte(signature)) == 1
}
import { Web3 } from "web3";
const web3 = new Web3();
// Moralis's own example hashes the parsed body serialised with JSON.stringify.
export function validMoralisSignature(req, secret) {
const received = req.get("x-signature");
if (!received) return false;
return web3.utils.sha3(JSON.stringify(req.body) + secret) === received;
}
The Go version hashes the raw bytes; Moralis's Node.js example re-serialises the parsed body. The two agree only while the body round-trips exactly through JSON.parse and JSON.stringify. If a signature fails on a payload that looks right, compare the inspector's Raw view with the re-serialised body.
Respond quickly
Delivery is at least once, so reply 200 as soon as the event is stored and make the handler idempotent: upsert on transactionHash, or on transactionHash plus logIndex for logs. That also absorbs the confirmed and unconfirmed pair.
Common mistakes
- Treating the signature as an HMAC. HMAC code, including the signature verifier on this site, never matches it.
- Using SHA3-256. Ethereum's Keccak-256 predates the final SHA-3 standard, and the two differ.
- Rejecting the test webhook. Any non-
2xxreply keeps the stream from starting. - Counting both deliveries, so every transfer is processed twice.
Try it with a real event
Create a test URL, use it as the stream's webhook URL, and watch the test webhook arrive as soon as you save the stream. Then trigger activity on a watched address to see the unconfirmed and confirmed deliveries.