Webhooks
Subscribe to signed HTTP callbacks for the lifecycle of every transaction you submit through /v1/swap (and, going forward, /v1/send).
All webhook endpoints require a self-service API key (vk_...). Manage keys at app.venum.dev.
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. Reached ~one block after processed. |
finalized | Rooted and irreversible. Reached ~12-13s after processed. |
failed | Your tx errored on chain. Terminal — mutually exclusive with the commitment events for the same signature. |
address-tx | An on-chain transaction involves a registered address. See POST /v1/webhooks/:id/track-address. Different payload shape from the commitment events. |
The commitment-tier events form the transaction webhook product — subscribe to them, then register signatures with /track-tx or auto-fire by submitting through /v1/swap/submit. The event taxonomy matches the /v1/stream/tx SSE stream so a webhook + SSE consumer of the same signature see consistent state.
address-tx is the address webhook product — subscribe to it alone, then register addresses with /track-address. Use a separate webhook row from the commitment events; don't mix the two on the same row.
POST /v1/webhooks
Create a webhook subscription. The webhook secret is returned exactly once in this response — capture it now or rotate by deleting and re-creating.
Request
POST /v1/webhooks
Content-Type: application/json
X-API-Key: vk_...Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Public HTTPS URL where events will be POSTed. Localhost / private network hostnames are rejected — for local dev, expose your handler via an ngrok or cloudflared tunnel. Max 512 chars. |
events | string[] | No | Subset of event types to subscribe to. Defaults to ["processed"]. This array also determines the webhook's kind — see below. |
Kind is implicit in events
There is no kind field. The webhook's kind is inferred from the event types:
eventscontains any oflanded/processed/confirmed/finalized/failed→ Transaction webhook. Pair withPOST /v1/webhooks/:id/track-txor auto-fire from/v1/swap/submit.eventsis["address-tx"]→ Address webhook. Pair withPOST /v1/webhooks/:id/track-address.
Mixing both in one webhook is technically accepted but discouraged — your handler then has to branch on two payload shapes. Create one webhook per kind.
Example
{
"url": "https://my-app.example.com/webhooks/venum",
"events": ["processed", "finalized"]
}Response 201
{
"id": 42,
"url": "https://my-app.example.com/webhooks/venum",
"events": ["processed", "finalized"],
"createdAt": "2026-05-17T20:00:00.000Z",
"pausedAt": null,
"secret": "whsec_3f9d1c8a2b7e0f5d6a4c8b2e9f1d3a7c5b8e0f2d4a6c8b1e"
}WARNING
The secret is shown once. Store it now — there is no endpoint to retrieve it later.
Errors
| Status | Description |
|---|---|
400 | Missing/malformed url, unknown event type, or no dbApiKeyId (env-keyed callers cannot subscribe). |
401 | Missing or invalid API key. |
409 | Webhook limit reached (20 per key). |
GET /v1/webhooks
List every webhook on the calling key. Secrets are not returned.
Response 200
{
"webhooks": [
{
"id": 42,
"url": "https://my-app.example.com/webhooks/venum",
"events": ["processed", "finalized"],
"createdAt": "2026-05-17T20:00:00.000Z",
"pausedAt": null
}
]
}PATCH /v1/webhooks/:id
Pause or resume a webhook. Paused webhooks remain in the list but do not receive events until resumed.
{ "paused": true }Response 200: returns the updated webhook (same shape as GET).
DELETE /v1/webhooks/:id
Permanently delete a webhook. The associated secret becomes unusable.
{ "deleted": true }404 if the webhook isn't found or isn't owned by the calling key.
POST /v1/webhooks/:id/test
Fire a synthetic event at the registered URL so you can verify your endpoint, HMAC validation, retry behaviour, and infrastructure end-to-end without waiting for real swap traffic.
The dispatch path is identical to production: same dispatch fan-out, same HMAC, same 3× exponential retry. Payload values are placeholders (prefixed test-) and include test: true so the receiver can branch.
Body
| Field | Type | Required | Description |
|---|---|---|---|
eventType | string | No | Which event type to fire — must be one your webhook subscribes to. Defaults to landed. |
Response 202
{
"dispatched": true,
"event": "landed",
"webhookId": 42,
"url": "https://my-app.example.com/webhooks/venum",
"testSignature": "test-1748707391000-abc12345"
}409 if the webhook is paused. 404 if not found / not owned.
POST /v1/webhooks/:id/track-tx
Register an arbitrary on-chain transaction signature for tracking. Venum polls getSignatureStatuses and fires the matching commitment events at the webhook URL as the tx walks through its lifecycle.
Use this for any tx — submitted through Venum's swap surface or broadcast elsewhere. Tracker is bounded to 60 seconds; auto-stops at the highest requested commitment.
Body
| Field | Type | Required | Description |
|---|---|---|---|
signature | string | Yes | Base58 transaction signature (~88 chars). |
events | string[] | No | Subset of processed / confirmed / finalized to fire. Default: all three. landed requires ShredStream and is rejected on this endpoint — use the swap-flow auto-fire for shred-tier signal. |
Response 202
{
"tracking": true,
"signature": "5KxabcDef...3nF",
"events": ["processed", "confirmed", "finalized"],
"webhookId": 42,
"url": "https://my-app.example.com/webhooks/venum",
"budgetMs": 60000
}Errors
| Status | Description |
|---|---|
400 | Invalid signature or unsupported event type. |
404 | Webhook not found or not owned. |
409 | Webhook is paused. |
503 | Server RPC connection unavailable. |
POST /v1/webhooks/:id/track-address
Register a Solana address against an address-tx-subscribed webhook. Venum polls getSignaturesForAddress every ~400 ms and fires for every new tx involving the address. End-to-end chain → webhook p50 latency is ~600 ms at the confirmed commitment level.
The webhook must subscribe to address-tx events on create (and only those — don't mix kinds).
Body
| Field | Type | Required | Description |
|---|---|---|---|
address | string | Yes | Base58 Solana pubkey (32–44 chars). |
label | string | No | Your identifier for the address (≤ 64 chars). Returned verbatim in every event. |
commitment | "confirmed" | "finalized" | No | Commitment level for the poll. Defaults to confirmed. See note below. |
commitment field
Per-subscription. Different addresses on the same webhook can run at different commitment levels.
| Value | Latency p50 | Use case |
|---|---|---|
confirmed (default) | ~600 ms | Fastest available signal. Near-zero re-org risk in practice. |
finalized | ~13 s | Tx is irreversible — the right choice for audit/accounting webhooks that must never fire on a re-org'd tx. |
processed is not accepted on this endpoint. Solana's RPC method getSignaturesForAddress only exposes confirmed/finalized; processed-tier signal will arrive when Venum moves address watching to WS push (logsSubscribe / blockSubscribe).
Response 201
{
"id": 1,
"webhookId": 42,
"address": "GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE",
"label": "Treasury",
"commitment": "confirmed",
"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."
}The first poll after create sets the cursor at the current newest signature for the address without firing events. Subsequent polls fire for every new signature. There is no historical backfill; if you need recent activity, call getSignaturesForAddress yourself.
Errors
| Status | Description |
|---|---|
400 | Invalid address (not base58 32–44 chars) or trivial address (System Program etc.) — see the guide for the rejected list. |
400 | Webhook does not subscribe to address-tx. |
402 | Tier does not support address tracking (anonymous keys). |
404 | Webhook not found or not owned. |
409 | Address already tracked by this webhook, or tier address-cap reached. |
503 | Platform-wide address-subscription cap reached. |
Tier limits
| Tier | Max addresses per webhook | Delivery cap |
|---|---|---|
| free | 1 | 100 deliveries/hour, then events dropped |
| starter | 5 | Unlimited |
| pro | 20 | Unlimited |
GET /v1/webhooks/:id/addresses
List active address subscriptions for the webhook.
Response 200
{
"addresses": [
{
"id": 1,
"address": "GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE",
"label": "Treasury",
"lastSeenSignature": "5xY9z…",
"commitment": "confirmed",
"createdAt": "2026-05-31T17:13:11.526Z"
}
]
}lastSeenSignature is null before the first poll; afterwards it tracks the most recent signature the poller has seen for that address.
DELETE /v1/webhooks/:id/addresses/:trackingId
Stop tracking an address. Soft-delete — the row is preserved with deleted_at set for audit, but the poller no longer fires for it.
{ "deleted": true }404 if not found or not owned.
Event payload
Each delivery POSTs a JSON body matching the subscribed event type:
POST /your/webhook/path
Content-Type: application/json
X-Venum-Signature: t=1715900000000,v1=4b7f...{
"type": "processed",
"signature": "5KxabcDef...3nF",
"slot": 254812345,
"inputMint": "So11111111111111111111111111111111111111112",
"outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"inputAmount": "1000000000",
"outputAmount": "143820000",
"dex": "orca",
"poolAddress": "Hp53XEt...UnxVtA",
"walletAddress": "9zT...kqW",
"quoteId": "q_abcd1234..."
}Common fields
| Field | Type | Description |
|---|---|---|
type | string | The event type — "landed" / "processed" / "confirmed" / "finalized" / "failed". |
signature | string | Base58 transaction signature. Use this as the idempotency key on your side. |
slot | number | Slot number where the event was observed. |
inputMint / outputMint | string | Token mint addresses. |
inputAmount / outputAmount | string | Raw token amounts (lamports / token base units), as strings to preserve precision. |
dex | string | null | Venue routed through (e.g. "orca", "raydium-clmm", "meteora-dlmm"). |
poolAddress | string | null | Pool routed through. For multi-hop routes, comma-joined. |
walletAddress | string | The user's wallet (signer of the swap). |
quoteId | string | The quoteId returned by /v1/swap/build. |
failed event
When type === "failed", the payload includes an err field with the on-chain error and no outputAmount (the tx didn't execute):
{
"type": "failed",
"signature": "5K...",
"slot": 254812345,
"err": "InstructionError: [3, { Custom: 6004 }]",
...
}address-tx event
Different shape from the commitment-tier payloads — no swap-specific fields, but includes the matched address and your label:
{
"type": "address-tx",
"signature": "5KxabcDef...3nF",
"slot": 332419471,
"address": "GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE",
"label": "Treasury",
"commitment": "confirmed",
"blockTime": 1748707391,
"err": null
}| Field | Type | Description |
|---|---|---|
type | string | Always "address-tx". |
signature | string | Base58 signature of the tx involving the address. |
slot | number | Slot the tx landed in. |
address | string | Which of your registered addresses matched. |
label | string | null | Your label for the address (verbatim). |
commitment | string | "confirmed" or "finalized" — whichever level this subscription was created at. |
blockTime | number | null | Unix seconds, may be null for very fresh blocks. |
err | string | null | On-chain error if the tx failed (non-null = failed tx that still touched the address). |
Call getTransaction(signature) if you need full tx details — accounts touched, instructions, balance changes, logs. The webhook payload is intentionally lightweight to keep delivery fast.
Signature header
Every webhook delivery includes an X-Venum-Signature header:
X-Venum-Signature: t=<unix_ms>,v1=<hex>| Component | Description |
|---|---|
t | Unix milliseconds when the payload was signed. |
v1 | Lowercase hex of HMAC_SHA256(secret, "<t>.<rawBody>"). |
Reject deliveries where t is older than your tolerance window (5 minutes recommended) before comparing v1. See the Webhooks guide for verification examples in Node and Python.
Delivery semantics
- At-least-once. Use the transaction
signatureas your idempotency key. The same(signature, type)pair will not be delivered more than once to any given webhook within the same process lifetime. - Retries. Failed deliveries retry up to 3 times with exponential backoff (~0.5s, ~1s, ~2s). Non-2xx responses count as failure.
- Timeout. Each delivery attempt times out after 5 seconds.
Rate limits
Webhook management endpoints share the dashboard rate-limit bucket:
| Tier | Limit |
|---|---|
| free | 30 / min |
| starter | 120 / min |
| pro | 300 / min |
