Test Helius webhooks
Helius webhooks deliver Solana transactions for the accounts you watch. Its documentation does not state the HTTP method or content type of a delivery, which is exactly what a capture answers: register a temporary URL as the webhookURL, and the first delivery shows the method, every header and the body as received.
Point your Helius webhook at it and see a real event.
What Helius sends
There are three webhook types. Enhanced webhooks send parsed, human-readable transactions such as NFT sales. Raw webhooks send unparsed transaction data with lower latency, but cannot be filtered by transaction type. Discord webhooks post formatted messages to a Discord channel. Each has a devnet variant.
Enhanced and raw webhooks both send a JSON array, one element per transaction, so loop even when you expect one. An enhanced transaction, trimmed and with placeholder values:
[ { "description": "", "type": "NFT_SALE", "fee": 5000, "feePayer": "11111111111111111111111111111111", "signature": "1111111111111111111111111111111111111111111111111111111111111111", "slot": 0, "timestamp": 0, "nativeTransfers": [ { "fromUserAccount": "11111111111111111111111111111111", "toUserAccount": "11111111111111111111111111111111", "amount": 0 } ], "tokenTransfers": [], "accountData": [], "events": {} }]
A raw webhook carries each transaction as the RPC returns it, with blockTime, indexWithinBlock, meta, slot and transaction.
Headers
| Header | Example value | Meaning |
|---|---|---|
| Authorization | Bearer 3f9c…e21a | The authHeader value you set on the webhook, echoed back unchanged. It is the only way to tell a delivery came from Helius. |
Helius does not document a signature header. Capture a delivery to see everything else it sends.
Verify the signature
Helius does not sign deliveries. Instead, set authHeader when you create or update the webhook, and compare the Authorization header of each delivery with that value in constant time. Pick a long random value and keep it out of your repository like any other secret.
import (
"crypto/subtle"
"net/http"
)
// fromHelius compares the echoed Authorization header with authHeader.
func fromHelius(r *http.Request, authHeader string) bool {
got := r.Header.Get("Authorization")
return subtle.ConstantTimeCompare([]byte(got), []byte(authHeader)) == 1
}
import { timingSafeEqual } from "node:crypto";
// fromHelius compares the echoed Authorization header with authHeader.
export function fromHelius(req, authHeader) {
const got = Buffer.from(req.get("authorization") ?? "");
const want = Buffer.from(authHeader);
return got.length === want.length && timingSafeEqual(got, want);
}
Respond quickly
Your endpoint must answer 200 within 1 second; a slower reply counts as a failure. Helius retries a delivery that gets a 5xx, a 4xx other than 403, a timeout or a connection error, up to 3 times with 1 second between attempts. After that the event is lost. The same event can also arrive more than once, so de-duplicate on the transaction signature.
A webhook whose failure rate reaches 95% is disabled automatically: over 7 days on paid plans and over 24 hours on the free plan.
Common mistakes
- Looking for a signature header. There isn't one. If
Authorizationis missing, the webhook was created withoutauthHeader. - Returning
403for a bad header and expecting a retry. Helius does not retry a403, which is right for a forged request but not for a misconfigured one. - Writing to a database or calling an RPC before replying. The 1-second budget leaves no room; queue the payload, reply, then process.
- Treating the body as one object. It is always an array.
Try it with a real event
Create a test URL, register it as a Helius webhook for an address you control, and send a transaction. The capture shows the method, content type and Authorization header that the docs leave out.