Skip to main content
Helius Webhooks Guide: Real-Time Solana Apps - Featured image
Blockchain

Helius Webhooks Guide: Real-Time Solana Apps

BUILDERZ

BUILDERZ

Share

In this guide

The decisions and working steps ahead.

    How Helius webhooks work

    Limits that shape your design

    Step 1: Create the webhook with the SDK

A user swaps on your app, and the balance doesn't move for a minute. Your backend is polling the chain and burning credits to find nothing new.

Webhooks push each matching transaction to your server instead. The catch: your server gets 1 second to answer, or the event can be lost.

Save this guide and use this receiver pattern. It's fast, safe against duplicates, and able to fill gaps.

How Helius webhooks work

You register a URL, some accounts, and the transaction types you care about.

When a matching transaction confirms, your URL gets an HTTP POST. The body is a JSON array of transactions, as the examples in the webhooks docs show.

You pick one of three webhook types:

  • Enhanced sends parsed events with fields like type, source, nativeTransfers, tokenTransfers, and events.
  • Raw sends the full transaction with meta and transaction fields, and the signature sits under transaction.signatures.
  • Discord posts formatted messages to a Discord channel.

The API reference also lists a devnet version of each type.

Limits that shape your design

The webhooks FAQ sets the rules. One webhook can watch up to 100,000 addresses when you create it through the API. The getting started guide caps dashboard-created webhooks at 25 addresses.

Each delivered event costs 1 credit, per the webhooks docs. The same page says raw webhooks have lower latency because they skip parsing. Pick enhanced when you want readable events, and raw when every millisecond counts.

There's also a newer option. The Parsed Streams page describes a WebSocket service that replaces Enhanced Transaction Webhooks for real-time parsed data. Webhooks still fit best when you want plain HTTP and no open socket.

Step 1: Create the webhook with the SDK

Start with a shared SDK client. Our Helius RPC Solana tutorial covers install, Node version, and key handling. helius-sdk 3.3.0 declares @solana/kit ^6.9.0 as a peer on the npm registry, so pin Kit inside that range instead of taking the latest major. The short version:

Code examplebash
pnpm add helius-sdk@3.3.0 @solana/kit@^6.9.0

The SDK's webhooks client exposes create, get, getAll, update, delete, and toggle, per the SDK source. Create one webhook per job, not one per user:

Code exampletypescript
// scripts/create-webhook.ts
import { createHelius } from "helius-sdk";

const helius = createHelius({ apiKey: process.env.HELIUS_API_KEY! });

const webhook = await helius.webhooks.create({
  webhookURL: "https://app.example.com/api/webhooks/helius",
  webhookType: "enhanced",
  transactionTypes: ["TRANSFER", "SWAP", "NFT_SALE"],
  accountAddresses: ["YOUR_TREASURY_WALLET", "YOUR_PROGRAM_ID"],
  txnStatus: "success",
  authHeader: process.env.HELIUS_WEBHOOK_SECRET,
});

console.log("Save this ID:", webhook.webhookID);

Pay attention to three fields.

  • transactionTypes values are listed on the transaction types page, with the sources behind each type.
  • txnStatus filters by result. The SDK types accept all, success, or failed.
  • authHeader is a secret you choose. The service sends it back in the Authorization header on every delivery.

Store the webhook ID in config. You'll need it to add addresses and to re-enable the webhook later.

Step 2: Answer every delivery in under 1 second

This is the rule that breaks most first versions. The webhooks FAQ says your endpoint must return a 200 within 1 second. Failed deliveries get up to 3 retries with a 1-second gap, and then the event is lost.

So your handler does three cheap things and stops. It checks the secret, saves the raw body, and returns 200. All the real work happens later, in a worker.

Here's a Next.js App Router route handler that does exactly that:

Code exampletypescript
// app/api/webhooks/helius/route.ts
import { timingSafeEqual } from "node:crypto";
import { db } from "@/lib/db";

function authorized(header: string | null) {
  const expected = Buffer.from(process.env.HELIUS_WEBHOOK_SECRET ?? "");
  const given = Buffer.from(header ?? "");
  return expected.length > 0 && given.length === expected.length && timingSafeEqual(given, expected);
}

export async function POST(req: Request) {
  if (!authorized(req.headers.get("authorization"))) {
    return new Response("unauthorized", { status: 401 });
  }

  const events: Array<{ signature: string }> = await req.json();

  await db.query(
    `insert into helius_events (signature, payload)
     select e->>'signature', e from jsonb_array_elements($1::jsonb) as e
     on conflict (signature) do nothing`,
    [JSON.stringify(events)]
  );

  return new Response("ok", { status: 200 });
}

One insert per delivery keeps the handler fast. The on conflict do nothing clause also absorbs duplicates before they reach your logic.

Don't respond first and process later on serverless

A common pattern sends the 200 and then keeps working in the same function. On a long-running server, that works.

On serverless hosts, the function can freeze once the response goes out, so the work never finishes. Persist first, respond second, and let a worker do the rest.

Step 3: Make processing idempotent

The FAQ warns that you can get the same event more than once, from retries or network issues. It recommends tracking the transaction signature to spot repeats. The table from step 2 does that with a primary key:

Code exampletext
create table helius_events (
  signature    text primary key,
  payload      jsonb not null,
  received_at  timestamptz not null default now(),
  processed_at timestamptz
);

create index helius_events_pending on helius_events (received_at)
  where processed_at is null;

A worker then picks up unprocessed rows, handles each type, and marks them done:

Code exampletypescript
// worker/process-helius-events.ts
import { db } from "@/lib/db";

type EnhancedTx = {
  signature: string;
  type: string;
  nativeTransfers?: { fromUserAccount: string; toUserAccount: string; amount: number }[];
};

export async function processBatch() {
  const { rows } = await db.query(
    `select signature, payload from helius_events
     where processed_at is null order by received_at limit 100
     for update skip locked`
  );

  for (const row of rows) {
    const tx = row.payload as EnhancedTx;
    switch (tx.type) {
      case "TRANSFER":
        await creditDeposits(tx);
        break;
      case "SWAP":
      case "NFT_SALE":
        await recordActivity(tx);
        break;
      default:
        console.warn("unhandled type", tx.type, tx.signature);
    }
    await db.query("update helius_events set processed_at = now() where signature = $1", [tx.signature]);
  }
}

Run the worker in a transaction so for update skip locked holds. Then two workers never grab the same row.

Each handler should also be safe to run twice, for example by keying ledger entries on the signature. Add an attempts counter too. After a few failures, move the row to a dead-letter state and alert a person, so one bad payload can't block the queue.

Amounts in nativeTransfers are in lamports. Divide by 1,000,000,000 for SOL, and keep the integer value in your database.

Step 4: Run it in production

Webhooks don't need much care once they work, but they fail quietly. The webhooks docs say endpoint health is checked every 4 hours. A webhook with a failure rate of 95% or more gets disabled, measured over 7 days on paid plans and 24 hours on the Free plan.

Watch for that. A release that breaks your route can switch off your event feed without an error on your side. After you fix the endpoint, call helius.webhooks.toggle(id, true) to turn it back on.

Run this checklist before a webhook goes live:

  1. HTTPS endpoint with the auth header check and a 401 on mismatch.
  2. Handler returns 200 in well under 1 second, measured from your logs.
  3. Signature primary key, so duplicates are a no-op.
  4. A worker with retries and a dead-letter state for rows that keep failing.
  5. An alert when no events arrive for longer than your normal quiet period.
  6. A nightly reconcile job that fetches recent history for your key accounts and fills gaps.
  7. Separate webhooks and secrets for devnet and mainnet.

The reconcile job matters because lost events are final. For mainnet, our RPC tutorial shows how to fetch parsed history through Parsed Events. Compare those signatures with your table and insert what's missing.

FAQ

How fast must my Helius webhook endpoint respond?

Your endpoint must return a 200 within 1 second. Failed deliveries get up to 3 retries with a 1-second gap, then the event is dropped. Save the payload and respond, then process it in a worker.

How many addresses can one Helius webhook watch?

One webhook can watch up to 100,000 addresses when you create it through the API or SDK. Webhooks made in the dashboard are capped at 25 addresses. For more, update the address list with the SDK.

Can I receive the same webhook event twice?

Yes, the same event can arrive more than once. The webhooks FAQ asks you to make handlers idempotent and track the transaction signature. A primary key on the signature turns duplicates into a no-op.

How do I check that a webhook request came from Helius?

Set an authHeader when you create the webhook. That value comes back in the Authorization header on every delivery. Compare it with a timing-safe check and return 401 on a mismatch.

Do Helius webhooks work on devnet?

Yes, use the enhancedDevnet, rawDevnet, or discordDevnet webhook type. In the dashboard, choose devnet as the network. Keep a separate secret for devnet, so test traffic can't reach production.

Build your real-time feed with Builderz

You now have a webhook that answers in time, stores every event once, and recovers from gaps. The same pattern works for deposits, trades, mints, and game state.

Our SEND FOMO case study covers a Solana NFT game with Tensor and Magic Eden integrations, an auto-compounding prize pool, and a burn-to-claim exit. Marketplace sales and burns like those are events an enhanced webhook can report as NFT_SALE or BURN_NFT.

The transaction types list maps NFT_SALE to both Tensor and Magic Eden. A receiver like this one keeps a game UI or leaderboard current without polling.

Our web development services cover the Next.js routes, queues, and database around a feed like this. Our Solana development service covers the programs and accounts it watches.

Try it on devnet with one wallet first. When you want us to build or review the production version, tell us about your Solana project.

Continue reading

Want to turn your idea into reality?

Book a 20-minute intro call, or send a short project brief. We reply within one business day.