Create an API key, point a stream rule at your endpoint, and verify every webhook signature. Three steps and you're receiving real-time Hedera smart contract events you can fully trust.
Your API key authenticates calls to the Rules API. Create one from the console:
Open the API keys page in the console.
Click New key, give it a descriptive name, and create it.
Copy the key and use it as your Authorization header.
Send the key as the Authorization header on every Rules API request. Treat it like a password — never commit it or expose it in client-side code.
A stream rule watches a contract and delivers matching events to your webhook. Set one up from the console:
Go to Streams and pick your network (mainnet or testnet).
Click New rule.
Enter a rule name, the predicate value (the contract ID to watch, e.g. 0.0.48670965), and your webhook URL.
Leave the rule enabled and click Create rule. Matching events start flowing immediately.
POST the same rule to the Rules API with your key in the Authorization header. ruleType: 4 subscribes to contract calls by contract ID.
URL: https://mainnet.streams-api.hashstream.xyz/rules
Method: POST
Headers:
Authorization: <your HashStream API key>
Content-Type: application/json
Body:
{
"chain": "hedera",
"ruleType": 4,
"predicateValue": "0.0.48670965",
"ruleName": "My contract event stream",
"actionWebhookUrl": "https://api.you.com/events"
}See the Rules API reference for the full request schema and every available field.
Every outbound webhook is signed with an ECDSA P-256 (ES256) signature so you can confirm it came from HashStream and was not tampered with in transit. Your endpoint is public, so verify the signature on every request and reject anything that does not pass.
| Header | Purpose |
|---|---|
| x-hashstream-signature | Base64-encoded ECDSA P-256 signature (DER format) |
| x-hashstream-signature-timestamp | Unix milliseconds at the time the request was signed |
| x-hashstream-signature-key-id | kid of the public key that should verify this signature |
| x-hashstream-signature-version | Signature scheme version. Currently v1 |
The signing string is ${timestamp}.${rawBody} where rawBody is the exact bytes of the request body. Verify against the bytes you receive on the wire — not against a re-serialized copy of the parsed JSON, which may differ byte-for-byte.
Only the signature authenticates the body. Other x-hashstream-* headers (event id, request id, retry balance) are unsigned advisory metadata. Any value you rely on to decide what to do with a webhook — which rule it belongs to, which network it came from — must come from the verified body (e.g. metadata.rule.id, metadata.network), never from request headers.
Fetch the public keys from the JWKS endpoint for your network. It is public, unauthenticated, and served from CloudFront — independent of the HashStream API.
GET https://keys.hashstream.xyz/networks/<network>/.well-known/jwks.json
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "...",
"kid": "<keyId>",
"use": "sig",
"alg": "ES256"
}
]
}cache-control where present).x-hashstream-signature-key-id not in your cache, re-fetch the JWKS once before rejecting — that is the key-rotation signal. During a rotation overlap both the new and previous keys are returned.Treat any request whose x-hashstream-signature-timestamp is more than 5 minutes from the current time as stale, and skip duplicate x-hashstream-event-id values you have already processed.
HashStream retries any non-2xx response. A request that fails signature verification should fail closed with 401 — it is untrusted and you want it gone. But a stale or duplicate request that already verified is one you've effectively handled — returning an error there just triggers pointless retries. Either return 200 to acknowledge it, or return your error alongside the x-hashstream-no-retry: true response header to stop retries explicitly.
import { createVerify, createPublicKey } from "node:crypto";
// rawBody MUST be the exact bytes received on the wire — never a
// re-serialized copy of the parsed JSON.
function verify(headers, rawBody, jwks) {
const sig = headers["x-hashstream-signature"];
const ts = headers["x-hashstream-signature-timestamp"];
const kid = headers["x-hashstream-signature-key-id"];
// Reject anything more than 5 minutes from now (replay protection).
if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return false;
const jwk = jwks.keys.find((k) => k.kid === kid);
if (!jwk) return false; // unknown kid → re-fetch JWKS once, then reject
const pubKey = createPublicKey({ key: jwk, format: "jwk" });
const verifier = createVerify("SHA256");
verifier.update(`${ts}.${rawBody}`);
verifier.end();
return verifier.verify(pubKey, Buffer.from(sig, "base64"));
}A complete, deployable AWS Lambda that verifies every inbound HashStream webhook against the published JWKS — including JWKS caching, key rotation, and skew checks.
hashstream-consumer-node on GitHub→Full protocol spec: webhook-signatures.md.
Every Rules API endpoint, request, and response — documented in the OpenAPI specs for each network.
Production API reference for mainnet streams.
Sandbox API reference for testnet streams.
Have a question the docs don't answer? Email us at support@hashstream.xyz.