Guide to Helius Webhooks: Features and Best Practices
Helius webhooks push Solana activity to your application as it happens: a wallet you watch receives tokens, an NFT sells, a swap executes, a stake moves. If you build trading bots, wallet notifications, NFT feeds or indexers on Solana, webhooks save you from polling the chain or holding open subscriptions for every address you care about.
This guide covers the types of Helius webhook, how to create one, how to authenticate deliveries when Helius doesn't sign them, what the payload looks like, the retry and auto-disable rules, and the best practices for running a Helius receiver in production.
What are Helius webhooks?
A Helius webhook watches a set of Solana addresses and sends an HTTP POST to your URL when transactions involve them. Each webhook has a type:
- Enhanced webhooks send parsed, human-readable transactions, each with a
typesuch asNFT_SALEorSWAP. You can filter them by transaction type. - Raw webhooks send unparsed transaction data for every transaction involving your addresses. They "do not involve parsing event types", which makes them lower latency, and they cannot be filtered by type.
- Discord webhooks post formatted messages to a Discord channel instead of your server.
Each type also has a devnet variant. For new projects that need parsed transactions, Helius now points to Parsed Streams, which it says "succeeds Enhanced Transaction Webhooks".
Helius does not sign webhook payloads. The only authentication is an optional authHeader value you set on the webhook, which Helius sends as the Authorization header on every delivery.
Helius webhook features
| Feature | Details |
|---|---|
| Configuration | Helius dashboard, POST /v0/webhooks, or the TypeScript and Rust SDKs |
| Types | enhanced, raw, discord, plus enhancedDevnet, rawDevnet, discordDevnet |
| Addresses | Up to 100,000 per webhook |
| Webhook limit | 5 on the free plan, 50 on paid plans |
| Filtering | transactionTypes on enhanced webhooks. Empty means all |
| Authentication | Optional authHeader, sent verbatim as Authorization |
| Signature | None |
| Body | A JSON array of transactions |
| Event field | type on each enhanced transaction. None on raw |
| Response window | 200 within 1 second (Webhooks FAQ) |
| Retries | Documented differently on two pages. See below |
| Auto-disable | 95% or higher failure rate over 7 days (paid) or 24 hours (free) |
| Cost | 1 credit per event delivered, accepted or not. 100 credits per create, edit or delete call |
Common events
Enhanced transactions carry a type. Helius's transaction types page lists well over a hundred, and its API reference describes the full set as "500+ types". These are the ones most integrations start with:
| Type | Fires when |
|---|---|
TRANSFER | SOL or tokens move between accounts |
SWAP | A token swap executes, for example on Jupiter, Raydium or Orca |
NFT_SALE | An NFT sells on a marketplace |
NFT_LISTING, NFT_CANCEL_LISTING | An NFT is listed or delisted |
NFT_BID, NFT_BID_CANCELLED | A bid is placed or cancelled |
NFT_MINT | An NFT is minted |
TOKEN_MINT | A token is minted. Helius notes this only fires for specific programs, such as Candy Machine V1 and the Solana Program Library |
Each element of the array is one transaction. Here is the shape of an enhanced SWAP, from Helius's API reference:
[
{
"description": "WALLET_A swapped 1.5 SOL for 225.5 USDC on Jupiter",
"type": "SWAP",
"source": "JUPITER",
"fee": 5000,
"feePayer": "WALLET_A...",
"signature": "5wJb...",
"slot": 123456789,
"timestamp": 1704067200,
"tokenTransfers": [
{
"fromUserAccount": "WALLET_A...",
"toUserAccount": "POOL...",
"tokenAmount": 1500000000,
"mint": "So11111111111111111111111111111111111111112"
}
],
"accountData": [
{
"account": "WALLET_A...",
"nativeBalanceChange": -1500005000,
"tokenBalanceChanges": []
}
]
}
]
source names the program or marketplace, signature is the Solana transaction signature, timestamp is in Unix seconds, and amounts are in base units (lamports for SOL). Parsed sub-events sit under events, such as events.nft for an NFT sale. One delivery can carry several transactions of different types.
Setting up Helius webhooks
- Generate a random token, for example with
openssl rand -hex 32, and decide on the full header value, such asBearer <token>. Store that whole string server-side asHELIUS_AUTH_HEADER. - Create the webhook in the Helius dashboard, or through the API:
curl -X POST "https://mainnet.helius-rpc.com/v0/webhooks?api-key=$HELIUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhookURL": "https://example.com/webhooks/helius",
"webhookType": "enhanced",
"accountAddresses": ["YOUR_WALLET_ADDRESS"],
"transactionTypes": ["SWAP", "NFT_SALE", "TRANSFER"],
"authHeader": "'"$HELIUS_AUTH_HEADER"'"
}'
- Watch the webhook logs in the dashboard to confirm deliveries arrive and get
200responses.
Every create, edit or delete through the API costs 100 credits. To pause a webhook without deleting it, or to re-enable one Helius disabled, call PATCH /v0/webhooks/{webhookID} with {"active": true} or false.
For local development, the Hookdeck CLI (hookdeck listen 3000 helius --path /webhooks/helius) gives you a public HTTPS URL that forwards to your local server, with no account required. Use it as the webhookURL.
Securing Helius webhooks
Helius sends your authHeader string unchanged as the Authorization header. Compare the whole header value with the one you stored, including any Bearer prefix, in constant time. Reject the request if the header is missing or wrong, and fail closed if your own value isn't configured.
The payload's signature field is easy to misread. It is the Solana transaction signature, which Helius recommends as the deduplication key, not a signature over the webhook body.
const crypto = require('crypto');
const express = require('express');
function verifyHeliusAuth(headerValue, expected) {
if (!expected || typeof headerValue !== 'string') return false; // fail closed
const received = Buffer.from(headerValue, 'utf8');
const stored = Buffer.from(expected, 'utf8');
return received.length === stored.length && crypto.timingSafeEqual(received, stored);
}
const app = express();
app.post('/webhooks/helius', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyHeliusAuth(req.get('authorization'), process.env.HELIUS_AUTH_HEADER)) {
return res.sendStatus(401);
}
const transactions = JSON.parse(req.body.toString('utf8'));
// Queue each transaction keyed on tx.signature, then answer straight away.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
def verify_helius_auth(header_value, expected) -> bool:
if not expected or not isinstance(header_value, str):
return False # fail closed
return hmac.compare_digest(header_value.encode("utf-8"), expected.encode("utf-8"))
# In FastAPI:
# verify_helius_auth(request.headers.get("authorization"), os.environ.get("HELIUS_AUTH_HEADER"))
A static header proves the sender knows the value. It does not protect the body or prevent a captured request being replayed, so use HTTPS and deduplicate on the transaction signature.
Make Helius webhooks production-ready. Hookdeck Event Gateway checks the
Authorizationheader on every Helius delivery, answers in well under a second, and durably queues each batch of transactions before your service sees it.
Helius webhook limitations and pain points
One second to answer, and conflicting retry rules
The Problem: Transactions that your endpoint was too slow for, or failed on, never arrive again.
Why It Happens: Helius's Webhooks FAQ says your endpoint "must return a 200 status code within 1 second", and that failed deliveries are retried "up to 3 times with a 1-second gap between each attempt". After that, "the event is permanently lost". The API reference, by contrast, says Helius "retries with exponential backoff for up to 24 hours". The FAQ is the more detailed of the two, and a few seconds of downtime is enough to exhaust its retries, so plan for that one.
Workarounds:
- Return
200straight after the header check and do everything else asynchronously. - Re-read recent transactions for your addresses after any outage, through Helius's APIs or your own RPC calls, to fill gaps.
- Check the webhook logs in the dashboard for failed deliveries.
How Hookdeck Can Help: A Hookdeck source answers Helius immediately and stores every delivery. Your service then gets the transactions with retries on your own schedule, so a deploy or a slow database no longer drops on-chain events.
Webhooks disabled for failing
The Problem: A webhook stops sending entirely and shows as disabled in the dashboard.
Why It Happens: Helius disables webhooks with a failure rate of 95% or more, measured over 7 days on paid plans and 24 hours on the free plan, and checks every 4 hours. Users on the Dev plan and above get an email.
Workarounds:
- Fix the endpoint, then re-enable the webhook in the dashboard or with
PATCH /v0/webhooks/{webhookID}and{"active": true}. Helius then gives it a 24-hour grace period before it can be disabled again. - Alert on the email, and check the webhook list in the dashboard after any incident.
How Hookdeck Can Help: Because Hookdeck accepts deliveries for you, your own failures don't count against the webhook's failure rate. Hookdeck issues alert you when delivery to your service starts failing.
Duplicates
The Problem: The same transaction is processed twice and a user gets two notifications.
Why It Happens: Helius says it "may deliver the same webhook event multiple times" because of retries or network issues.
Workarounds:
- Store each transaction
signatureand skip ones you have seen, as Helius recommends. - Make downstream actions idempotent.
How Hookdeck Can Help: Hookdeck deduplication can drop repeated deliveries before they reach your service.
Credits are charged for every delivery
The Problem: Your credit usage climbs faster than the transactions you actually use.
Why It Happens: Each event costs 1 credit "regardless of whether your endpoint successfully processes the webhook or not", and every API create, edit or delete costs 100 credits.
Workarounds:
- Use
transactionTypeson enhanced webhooks to receive only the types you handle. - Batch address changes into fewer edit calls.
How Hookdeck Can Help: Hookdeck's request log shows exactly what Helius sent, so you can see which transaction types you are paying for and tighten the filter.
Best practices
Compare the whole Authorization value
Store the exact authHeader string, including Bearer , and compare it in full. Hookdeck's Helius source works the same way, so enter the complete value there too.
Iterate over the array
The body is always an array. Loop over it, route each element on type for enhanced webhooks, and keep a default branch for types you don't handle.
Deduplicate on the transaction signature
Use signature as the idempotency key, and store it before acting. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
With a 1-second budget, the handler should check the header, enqueue and return 200. See why to process webhooks asynchronously.
Treat amounts as base units
fee, nativeBalanceChange and transfer amounts are integers in the smallest unit. Convert at the edge of your system and keep the original values.
Conclusion
Helius sends Solana transactions as a JSON array, with no signature. Set an authHeader and compare the full Authorization value in constant time, loop over the transactions and route enhanced ones on type, deduplicate on the transaction signature, and answer within a second, because the FAQ allows only three quick retries before a delivery is lost.
Hookdeck Event Gateway checks the header, answers Helius immediately and holds every transaction until your service has processed it, so a slow handler or a deploy doesn't lose on-chain events or get your webhook disabled.
Get started with Hookdeck for free and handle Helius webhooks reliably in minutes.