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 webhook | Address webhook | |
|---|---|---|
| You hand Venum | A tx signature | A Solana address |
| Lifecycle | Auto-stops at finalized (~13s) | Runs forever until deleted |
| Use case | "Did my tx land?" | "Did anything happen to this address?" |
| Subscribe to | landed / processed / confirmed / finalized / failed | address-tx |
| Limits | Per-tier rate-limited | Per-tier address count |
| Helius comparable | None — closer to QuickNode | Helius "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/failed→ Transaction webhook - Subscribe to
address-tx→ Address 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.
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"]}'{
"id": 42,
"url": "https://your-app.example.com/webhooks/venum",
"events": ["processed", "confirmed", "finalized"],
"secret": "whsec_3f9d1c8a2b7e..."
}Event types
| Event | Meaning |
|---|---|
landed | Earliest 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. |
processed | The cluster has executed your tx. Default for new webhooks. Sub-second under typical conditions. |
confirmed | Supermajority commitment. ~one block after processed. |
finalized | Rooted and irreversible. ~12-13s after processed. |
failed | Your 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:
- 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. POST /v1/webhooks/:id/track-tx— pass any signature, regardless of how the tx was broadcast (your own wallet, public RPC, anywhere). Venum pollsgetSignatureStatusesand fires events as the tx walks through commitment levels.
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"]}'{
"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
{
"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
{
"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:
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:
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"}'{
"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:
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/1First-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
| Tier | Max addresses per webhook | Event delivery |
|---|---|---|
| Anonymous | Not supported | — |
| Free | 1 | Capped at 100 deliveries/hour |
| Starter | 5 | Unlimited |
| Pro | 20 | Unlimited |
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:
| Address | Program |
|---|---|
11111111111111111111111111111111 | System Program |
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA | SPL Token Program |
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb | SPL Token-2022 Program |
ComputeBudget111111111111111111111111111111 | Compute Budget Program |
ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL | Associated Token Account Program |
Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo | Memo Program v1 |
MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr | Memo Program v2 |
SysvarRent111111111111111111111111111111111 | Rent Sysvar |
SysvarC1ock11111111111111111111111111111111 | Clock 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
signatureon your side - Optionally filter what you act on (only mint X, only above some amount, etc.) by calling
getTransactionon 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)
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
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)
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:
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:
| Attempt | Delay 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/:idwith{"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'sswitchclean.
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:
# 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 theconfirmedcommitment) 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.
