# 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 `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

| 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:

```json
[
  {
    "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](https://dashboard.helius.dev/webhooks), or through the API:

```bash
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](/docs/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.

```javascript
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:

```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](/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](/docs/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](/docs/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](/docs/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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Helius webhooks reliably in minutes.