Skip to content

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

EventMeaning
landedEarliest signal — the network has seen your tx in a shred. Pre-processing: not yet executed. ~100ms after broadcast.
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.

Choosing one event vs many:

  • Trading bots / latency-sensitive UX — subscribe to landed for earliest UI feedback ("sent"), and processed to flip to "done".
  • Settlement / accounting — subscribe to confirmed (safe against short forks) or finalized (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

bash
# 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.

json
{
  "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)

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

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) {
      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)

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:

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

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.

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"}'

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.