Documentation

From zero to verified events.

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.

Step 01

Create an API key

Your API key authenticates calls to the Rules API. Create one from the console:

  1. 1

    Open the API keys page in the console.

  2. 2

    Click New key, give it a descriptive name, and create it.

  3. 3

    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.

Step 02

Create a stream rule

A stream rule watches a contract and delivers matching events to your webhook. Set one up from the console:

  1. 1

    Go to Streams and pick your network (mainnet or testnet).

  2. 2

    Click New rule.

  3. 3

    Enter a rule name, the predicate value (the contract ID to watch, e.g. 0.0.48670965), and your webhook URL.

  4. 4

    Leave the rule enabled and click Create rule. Matching events start flowing immediately.

Prefer the API?

POST the same rule to the Rules API with your key in the Authorization header. ruleType: 4 subscribes to contract calls by contract ID.

POST /ruleshttp
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.

Step 03

Verify webhook signatures

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.

Signature headers

HeaderPurpose
x-hashstream-signatureBase64-encoded ECDSA P-256 signature (DER format)
x-hashstream-signature-timestampUnix milliseconds at the time the request was signed
x-hashstream-signature-key-idkid of the public key that should verify this signature
x-hashstream-signature-versionSignature scheme version. Currently v1

What is signed

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.

Trust the verified body, not the headers

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.

Public keys (JWKS)

Fetch the public keys from the JWKS endpoint for your network. It is public, unauthenticated, and served from CloudFront — independent of the HashStream API.

JWKSjson
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 the JWKS for at least an hour (respect the response cache-control where present).
  • If a webhook arrives with a 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.

Replay protection

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.

Choose your status code carefully

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.

Verify in Node.js

verify.jsjavascript
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"));
}
Working sample

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.

API reference

Every Rules API endpoint, request, and response — documented in the OpenAPI specs for each network.

Have a question the docs don't answer? Email us at support@hashstream.xyz.