Gareth Wilson Gareth Wilson

Guide to Helius Webhooks: Features and Best Practices

Published


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 type such as NFT_SALE or SWAP. 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

FeatureDetails
ConfigurationHelius dashboard, POST /v0/webhooks, or the TypeScript and Rust SDKs
Typesenhanced, raw, discord, plus enhancedDevnet, rawDevnet, discordDevnet
AddressesUp to 100,000 per webhook
Webhook limit5 on the free plan, 50 on paid plans
FilteringtransactionTypes on enhanced webhooks. Empty means all
AuthenticationOptional authHeader, sent verbatim as Authorization
SignatureNone
BodyA JSON array of transactions
Event fieldtype on each enhanced transaction. None on raw
Response window200 within 1 second (Webhooks FAQ)
RetriesDocumented differently on two pages. See below
Auto-disable95% or higher failure rate over 7 days (paid) or 24 hours (free)
Cost1 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:

TypeFires when
TRANSFERSOL or tokens move between accounts
SWAPA token swap executes, for example on Jupiter, Raydium or Orca
NFT_SALEAn NFT sells on a marketplace
NFT_LISTING, NFT_CANCEL_LISTINGAn NFT is listed or delisted
NFT_BID, NFT_BID_CANCELLEDA bid is placed or cancelled
NFT_MINTAn NFT is minted
TOKEN_MINTA 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

  1. Generate a random token, for example with openssl rand -hex 32, and decide on the full header value, such as Bearer <token>. Store that whole string server-side as HELIUS_AUTH_HEADER.
  2. 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"'"
  }'
  1. Watch the webhook logs in the dashboard to confirm deliveries arrive and get 200 responses.

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 Authorization header 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 200 straight 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 signature and 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 transactionTypes on 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.