Skip to content

Webhooks

Venum delivers signed HTTP callbacks for two distinct workflows:

  • Transaction webhooks — register a tx signature, get pinged at each commitment level (landed / processed / confirmed / finalized) until the tx is rooted. Auto-expires after finalization. Works for any Solana tx — submitted through Venum or anywhere else.
  • Address webhooks — register a Solana address (wallet, token mint, program, anything), get pinged for every transaction that involves it. Persistent until you delete it.

Both kinds use the same POST /v1/webhooks to register the URL + HMAC secret. You pick which kind you're building by which event types you subscribe to. One webhook can be only one kind.

For the full endpoint reference, see API → Webhooks.

Pick a kind

Transaction webhookAddress webhook
You hand VenumA tx signatureA Solana address
LifecycleAuto-stops at finalized (~13s)Runs forever until deleted
Use case"Did my tx land?""Did anything happen to this address?"
Subscribe tolanded / processed / confirmed / finalized / failedaddress-tx
LimitsPer-tier rate-limitedPer-tier address count
Helius comparableNone — closer to QuickNodeHelius "Webhooks" ($99/mo Build plan)

How the kind is determined

There is no kind field on the create call — the kind is implicit in the events array you pass to POST /v1/webhooks:

  • Subscribe to any of landed / processed / confirmed / finalized / failedTransaction webhook
  • Subscribe to address-txAddress webhook

Don't mix kinds

A single webhook subscribed to both address-tx AND commitment events is supported by the API but discouraged — your handler has to branch on two different payload shapes for one URL. Create a separate webhook per kind. The dashboard enforces this; the curl API allows it for advanced users only.

The secret returned at create time is shown once. Store it alongside your other API credentials — there is no GET /v1/webhooks/:id/secret. Lost secrets require delete + recreate.


Transaction webhook

Create the webhook

Pick which commitment events you care about. The choice fixes the kind as Transaction.

bash
curl -X POST https://api.venum.dev/v1/webhooks \
  -H "X-API-Key: vk_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.example.com/webhooks/venum",
       "events": ["processed", "confirmed", "finalized"]}'
json
{
  "id": 42,
  "url": "https://your-app.example.com/webhooks/venum",
  "events": ["processed", "confirmed", "finalized"],
  "secret": "whsec_3f9d1c8a2b7e..."
}

Event types

EventMeaning
landedEarliest signal — the network has seen your tx in a shred. ~100ms after broadcast. Shred-stream-sourced; only fires for txs submitted through /v1/swap/submit.
processedThe cluster has executed your tx. Default for new webhooks. Sub-second under typical conditions.
confirmedSupermajority commitment. ~one block after processed.
finalizedRooted and irreversible. ~12-13s after processed.
failedYour tx errored on chain. Terminal — mutually exclusive with the commitment events for the same signature.

Tracking any signature with /track-tx

The webhook by itself doesn't track anything; you register signatures against it. There are two ways the signatures get attached:

  1. Auto-fire from /v1/swap/submit — txs you submit through Venum's swap surface auto-track without any extra API call. The dispatch is tied to the api_key that submitted.
  2. POST /v1/webhooks/:id/track-tx — pass any signature, regardless of how the tx was broadcast (your own wallet, public RPC, anywhere). Venum polls getSignatureStatuses and fires events as the tx walks through commitment levels.
bash
curl -X POST https://api.venum.dev/v1/webhooks/42/track-tx \
  -H "X-API-Key: vk_..." \
  -H "Content-Type: application/json" \
  -d '{"signature": "5xY9z…",
       "events": ["processed", "confirmed", "finalized"]}'
json
{
  "tracking": true,
  "signature": "5xY9z…",
  "events": ["processed", "confirmed", "finalized"],
  "webhookId": 42,
  "url": "https://your-app.example.com/webhooks/venum",
  "budgetMs": 60000
}

The tracker is bounded (60 s, exponential-backoff polling). After the highest requested commitment is reached the tracker stops automatically.

Payload — transaction events

json
{
  "type": "processed",
  "signature": "5xY9z…",
  "slot": 332419471,
  "source": "rpc",
  "err": null
}

failed events include the on-chain error string in err. Swap-flow auto-fired events include additional fields (inputMint, outputMint, inputAmount, outputAmount, dex, poolAddress, walletAddress).


Address webhook

Event type — address-tx

json
{
  "type": "address-tx",
  "signature": "5xY9z…",
  "slot": 332419471,
  "address": "GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE",
  "label": "Treasury",
  "blockTime": 1748707391,
  "err": null
}

Fires once per signature that involves the registered address. The label is whatever you set when adding the address (your own identifier — Venum doesn't interpret it).

Quick start

Create a webhook subscribed to address-tx:

bash
curl -X POST https://api.venum.dev/v1/webhooks \
  -H "X-API-Key: vk_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.example.com/webhooks/venum",
       "events": ["address-tx"]}'

Then register one or more addresses against it:

bash
curl -X POST https://api.venum.dev/v1/webhooks/42/track-address \
  -H "X-API-Key: vk_..." \
  -H "Content-Type: application/json" \
  -d '{"address": "GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE",
       "label": "Treasury"}'
json
{
  "id": 1,
  "webhookId": 42,
  "address": "GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE",
  "label": "Treasury",
  "createdAt": "2026-05-31T17:13:11.526Z",
  "note": "First poll establishes the cursor at the current newest signature without firing events. Subsequent activity fires `address-tx` webhooks."
}

List + remove:

bash
curl -H "X-API-Key: vk_..." \
  https://api.venum.dev/v1/webhooks/42/addresses

curl -X DELETE -H "X-API-Key: vk_..." \
  https://api.venum.dev/v1/webhooks/42/addresses/1

First-poll behaviour

The first poll after creating an address subscription establishes a cursor at the current newest signature and fires no events. This prevents a flood of historical activity from hitting your endpoint the moment you add an address. Subsequent polls (every ~400 ms) fire for every new signature involving the address. End-to-end chain → webhook p50 latency is ~600 ms at the default confirmed commitment.

If you need to backfill recent history, fetch it yourself via the standard getSignaturesForAddress JSON-RPC method.

Commitment level

Per-subscription. Pass commitment on POST /v1/webhooks/:id/track-address:

  • confirmed (default) — fastest available signal, near-zero re-org risk in practice. Right for sniping-adjacent and monitoring.
  • finalized — irreversible, ~13 s slower. Right for accounting or audit webhooks that must never fire on a re-org'd tx.

processed is not yet available — the underlying getSignaturesForAddress RPC method doesn't expose it. Push-based delivery (logsSubscribe / blockSubscribe) for processed-tier signal is on the roadmap.

Tier limits

TierMax addresses per webhookEvent delivery
AnonymousNot supported
Free1Capped at 100 deliveries/hour
Starter5Unlimited
Pro20Unlimited

Address tracking on the free tier is rate-limited; over-cap events are dropped (not queued) with a log line. Upgrade to Starter for unlimited delivery and 5x the address budget.

Footgun addresses — rejected on create

Tracking the System Program is technically valid but useless — every Solana transaction touches it. Same goes for the other always-touched programs. We reject these at create time:

AddressProgram
11111111111111111111111111111111System Program
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DASPL Token Program
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEbSPL Token-2022 Program
ComputeBudget111111111111111111111111111111Compute Budget Program
ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knLAssociated Token Account Program
Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNoMemo Program v1
MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHrMemo Program v2
SysvarRent111111111111111111111111111111111Rent Sysvar
SysvarC1ock11111111111111111111111111111111Clock Sysvar

Track a specific wallet, token mint, or DEX program instead.

High-volume addresses — be ready

Tracking a program like Jupiter v6 generates hundreds of events per minute. Venum delivers all of them — your endpoint needs to:

  • Handle the rate (no free webhook.site for production)
  • Idempotently dedup by signature on your side
  • Optionally filter what you act on (only mint X, only above some amount, etc.) by calling getTransaction on the signature

Token-mint and wallet addresses are typically fine. Program addresses are firehose territory — make sure that's what you want.


Verifying signatures

Every delivery — both kinds — includes an X-Venum-Signature header:

X-Venum-Signature: t=1715900000000,v1=4b7f...

Reconstruct the signed payload as <t>.<raw request body>, HMAC-SHA256 it with your webhook secret, and compare to v1 in constant time.

Node.js (built-in crypto)

ts
import { createHmac, timingSafeEqual } from 'node:crypto'

const TOLERANCE_MS = 5 * 60 * 1000  // reject events older than 5 minutes

function verifyVenumSignature(
  secret: string,
  rawBody: string,
  header: string,
): boolean {
  const parts = new Map<string, string>()
  for (const segment of header.split(',')) {
    const eq = segment.indexOf('=')
    if (eq > 0) parts.set(segment.slice(0, eq).trim(), segment.slice(eq + 1).trim())
  }
  const t = Number(parts.get('t'))
  const v1 = parts.get('v1')
  if (!Number.isFinite(t) || !v1) return false
  if (Math.abs(Date.now() - t) > TOLERANCE_MS) return false
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
  const provided = Buffer.from(v1, 'hex')
  if (provided.length !== expected.length) return false
  return timingSafeEqual(provided, expected)
}

TIP

Read the raw request body before any JSON parsing. The signature is computed over the exact bytes Venum sent — re-serializing parsed JSON will not produce the same string.

Express example — handling both kinds

ts
import express from 'express'

const app = express()

app.post(
  '/webhooks/venum',
  express.raw({ type: 'application/json' }),  // get the raw Buffer
  (req, res) => {
    const signature = req.header('X-Venum-Signature') ?? ''
    const rawBody = req.body.toString('utf8')
    if (!verifyVenumSignature(process.env.VENUM_WEBHOOK_SECRET!, rawBody, signature)) {
      return res.status(401).send('invalid signature')
    }

    const event = JSON.parse(rawBody)
    switch (event.type) {
      // Transaction events
      case 'landed':
      case 'processed':
        // earliest UI feedback — flip a status
        break
      case 'confirmed':
      case 'finalized':
        // safe to credit / persist
        break
      case 'failed':
        // surface the error
        break

      // Address event
      case 'address-tx':
        // event.address  → which subscribed address matched
        // event.label    → your identifier for it
        // event.signature → the new tx involving it
        // call getTransaction(event.signature) if you need the full tx
        break
    }

    res.status(200).send('ok')
  },
)

Python (hmac + hashlib)

python
import hmac
import hashlib
import time

TOLERANCE_MS = 5 * 60 * 1000

def verify_venum_signature(secret: str, raw_body: bytes, header: str) -> bool:
    parts = {}
    for segment in header.split(','):
        if '=' in segment:
            k, v = segment.split('=', 1)
            parts[k.strip()] = v.strip()
    try:
        t = int(parts.get('t', ''))
    except ValueError:
        return False
    v1 = parts.get('v1')
    if not v1:
        return False
    if abs(int(time.time() * 1000) - t) > TOLERANCE_MS:
        return False
    signed = f'{t}.{raw_body.decode("utf-8")}'.encode('utf-8')
    expected = hmac.new(secret.encode('utf-8'), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Idempotency

Venum guarantees at-least-once delivery per (signature, event) pair. Network blips, your endpoint returning a 5xx, or an internal retry can each cause the same event to arrive more than once. Make your handler safe to call repeatedly.

The transaction signature plus the event type is the recommended idempotency key — works identically for both webhook kinds:

ts
async function handleEvent(event: VenumWebhookEvent): Promise<void> {
  const inserted = await db.query(
    `INSERT INTO processed_events (signature, event_type) VALUES ($1, $2)
       ON CONFLICT DO NOTHING`,
    [event.signature, event.type],
  )
  if (inserted.rowCount === 0) return  // already processed
  // ... your real business logic
}

For address-tx events, also consider (signature, address) if you need to handle the same tx hitting multiple addresses you watch.

Retries

If your endpoint returns a non-2xx status, doesn't respond within 5 seconds, or fails network-side, Venum retries up to 3 times total with exponential backoff:

AttemptDelay before next
1 → 2~500ms
2 → 3~1s
3 → give up

After the final attempt the delivery is dropped. Production-grade persistence (durable retries beyond the immediate window, plus a dead-letter you can poll) is a follow-up release.

Recommendations

  • Return 2xx quickly. Do the minimum work to validate the signature and enqueue the event for async processing — don't run business logic inline. A slow handler increases the chance of timeout-triggered retries.
  • Always verify the signature. Anyone who learns your webhook URL can POST to it. The HMAC signature is what proves the payload came from Venum.
  • Reject stale timestamps. A 5-minute tolerance window protects against replay attacks if an attacker captures one of your webhook payloads.
  • Pause, don't delete, when debugging. Use PATCH /v1/webhooks/:id with {"paused": true} to silence a webhook temporarily without losing its configuration or any registered signatures/addresses.
  • Don't mix kinds. Use one webhook for address-tx, a separate one for the commitment events. Keeps your handler's switch clean.

Local development

Venum POSTs events from cloud infrastructure to your URL, so the endpoint has to be reachable from the public internet. localhost, loopback (127.0.0.1), and private-network hostnames (10.x.x.x, 192.168.x.x, 172.16-31.x.x) are rejected on create.

The simplest local-dev workflow is to expose your handler via a tunnel:

bash
# Start your local handler on whatever port (3000 here)
node my-webhook-handler.js

# Open a public tunnel in another terminal
ngrok http 3000
#   → forwarding https://abcd1234.ngrok-free.app -> http://localhost:3000

# Point Venum at the tunnel URL
curl -X POST https://api.venum.dev/v1/webhooks \
  -H "X-API-Key: vk_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://abcd1234.ngrok-free.app/webhooks/venum",
       "events": ["address-tx"]}'

cloudflared tunnel, localtunnel, webhook.site (testing only — free tier rate-limits aggressively), and any other public-URL service work the same way.

What's next

  • Durable delivery with replay and a dead-letter queue
  • Per-event filtering on address-tx (only fire if signature includes program X, only above token amount Y, etc.) to cut down receiver load on program-level subscriptions
  • WS push for address watching — processed-tier signal + sub-second p50 latency (current polling caps out at ~600 ms p50 with the confirmed commitment) once we deploy the self-hosted node

Need an event the current set doesn't cover? Open a request via Discord or hello@venum.dev.