Webhooks
Webhooks deliver signed HTTP callbacks at each step of a transaction's on-chain lifecycle. Subscribe to one or more events per webhook — landed, processed, confirmed, finalized, failed — and Venum POSTs to your endpoint whenever your transactions hit that stage.
For the full endpoint reference, see API → Webhooks.
Event types
| Event | Meaning |
|---|---|
landed | Earliest signal — the network has seen your tx in a shred. Pre-processing: not yet executed. ~100ms after broadcast. |
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. |
Choosing one event vs many:
- Trading bots / latency-sensitive UX — subscribe to
landedfor earliest UI feedback ("sent"), andprocessedto flip to "done". - Settlement / accounting — subscribe to
confirmed(safe against short forks) orfinalized(irreversible). - Anti-double-spend / on-chain hooks — subscribe to
finalized.
Subscribing to multiple events for the same webhook is encouraged — the URL is called once per (signature, event) pair, dedup'd internally.
Quick start
# 1. Create a webhook subscription. Defaults to events=["processed"]
# if you don't specify.
curl -X POST https://api.venum.dev/v1/webhooks \
-H "Content-Type: application/json" \
-H "X-API-Key: vk_..." \
-d '{"url": "https://your-app.example.com/webhooks/venum"}'
# 2. Or subscribe to multiple events on create.
curl -X POST https://api.venum.dev/v1/webhooks \
-H "Content-Type: application/json" \
-H "X-API-Key: vk_..." \
-d '{"url": "https://your-app.example.com/webhooks/venum",
"events": ["landed", "processed", "confirmed", "failed"]}'The response includes a one-time secret — capture it and store it alongside your other API credentials.
{
"id": 42,
"url": "https://your-app.example.com/webhooks/venum",
"events": ["processed"],
"secret": "whsec_3f9d1c8a2b7e..."
}Submit a swap through /v1/swap. When it hits each subscribed event, Venum POSTs a payload to your URL, signed with the secret above.
Verifying signatures
Every delivery 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
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) {
case 'landed':
// earliest UI feedback — flip a status to "sent"
break
case 'processed':
// the cluster has executed — flip to "done"
break
case 'confirmed':
// safe to credit the user's balance, etc.
break
case 'finalized':
// safe for on-chain trust assumptions
break
case 'failed':
// surface the error to the user
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:
async function handleEvent(event: VenumWebhookEvent): Promise<void> {
// Reject duplicates at the database layer — INSERT ... ON CONFLICT
// DO NOTHING is simpler than checking + branching on the result.
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
}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.
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"}'cloudflared tunnel, localtunnel, webhook.site, and any other public-URL service work the same way. Once the webhook is created, run a swap through /v1/swap and watch the events hit your local handler.
What's next
The current event set covers the submit lifecycle for transactions you send through Venum. Upcoming releases will add:
- Address-based webhooks (subscribe to activity on arbitrary on-chain addresses, not just txs you submitted)
- Durable delivery with replay and a dead-letter queue
Need an event the current set doesn't cover? Open a request via Discord or hello@venum.dev.
