Set Up Webhooks
Webhooks let you receive real-time notifications when agent events occur — tier changes, reputation updates, status changes, and more. This tutorial covers creating a subscription, verifying signatures, and building a receiver.
Prerequisites
Install the required npm packages before proceeding:
npm install express crypto
Available Event Types
Fetch the list of available events from the public endpoint:
# Fetch the list of event types you can subscribe to
curl https://api.vouchprotocol.xyz/webhooks/event-types
{
"eventTypes": [
"agent.registered",
"agent.updated",
"agent.reputation_updated",
"agent.tier_updated",
"agent.status_changed",
"agent.suspended",
"agent.reactivated",
"agent.deactivated",
"agent.closed",
"stake.deposited",
"stake.withdrawn",
"stake.withdrawal_requested",
"stake.withdrawal_cancelled",
"stake.closed"
]
}
| Event | Fired When |
|---|---|
| agent.registered | A new agent is registered |
| agent.updated | Agent metadata is updated |
| agent.reputation_updated | Agent reputation score changes |
| agent.tier_updated | Agent tier changes due to deposit/withdrawal |
| agent.status_changed | Agent status changes (active/inactive) |
| agent.suspended | An agent is suspended |
| agent.reactivated | A suspended agent is reactivated |
| agent.deactivated | An agent is deactivated |
| agent.closed | An agent account is closed |
| stake.deposited | USDC deposited to an agent's vault |
| stake.withdrawn | USDC withdrawn from an agent's vault |
| stake.withdrawal_requested | A withdrawal is requested |
| stake.withdrawal_cancelled | A pending withdrawal is cancelled |
| stake.closed | A stake account is closed |
Step 1 — Wallet Authentication
All webhook management endpoints require wallet authentication. You need to sign a message with your Solana wallet and include three headers with every request.
Signed Message Format
vouch-auth:<pubkey>:<timestamp>:<method>:<path>
# Example for POST /webhooks:
vouch-auth:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU:1710000000:POST:/webhooks
Required Headers
| Header | Value |
|---|---|
| X-Wallet-Pubkey | Your Solana public key (base58) |
| X-Wallet-Signature | Base58-encoded Ed25519 signature of the message |
| X-Wallet-Timestamp | Unix timestamp (seconds) used in the signed message |
import { Keypair } from '@solana/web3.js'; // Solana keypair for signing import nacl from 'tweetnacl'; // Ed25519 signing library import bs58 from 'bs58'; // Base58 encoding for Solana-compatible signatures function createAuthHeaders( wallet: Keypair, method: string, path: string, ): Record<string, string> { const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp in seconds const pubkey = wallet.publicKey.toBase58(); // Base58 wallet address const message = `vouch-auth:${pubkey}:${timestamp}:${method}:${path}`; // Canonical auth message format const msgBytes = new TextEncoder().encode(message); // Convert to bytes for signing const signature = nacl.sign.detached(msgBytes, wallet.secretKey); // Ed25519 detached signature return { 'X-Wallet-Pubkey': pubkey, // Identifies the signing wallet 'X-Wallet-Signature': bs58.encode(signature), // Proof of ownership 'X-Wallet-Timestamp': timestamp.toString(), // Prevents replay attacks }; }
Step 2 — Create a Webhook Subscription
const headers = createAuthHeaders(wallet, 'POST', '/webhooks'); // Generate wallet auth headers const res = await fetch('https://api.vouchprotocol.xyz/webhooks', { // Create a new webhook subscription method: 'POST', headers: { 'Content-Type': 'application/json', ...headers, // Spread the auth headers into the request }, body: JSON.stringify({ url: 'https://your-server.com/webhooks/vouch', // Your server's webhook endpoint eventTypes: [ // Subscribe to specific event types 'agent.tier_updated', 'agent.reputation_updated', 'agent.status_changed', ], // Optional: filter events for a specific agent PDA // agentPda: 'AgentPDA...' }), }); const webhook = await res.json(); console.log('Webhook ID:', webhook.id); // Unique subscription identifier console.log('HMAC Secret:', webhook.secret); // Used to verify incoming payloads // IMPORTANT: Save this secret! It is shown only once.
{
"id": "wh_abc123",
"secret": "whsec_k7G9mN2x...",
"message": "Save the secret -- it will not be shown again."
}
Save the secret immediately. The HMAC signing secret is returned only once when you create the webhook. Store it securely — you will need it to verify incoming webhook payloads.
Step 3 — Webhook Payload Structure
When an event fires, Vouch sends a POST request to your URL with this payload:
{
"id": "evt_abc123",
"type": "agent.tier_updated",
"pda": "BKq8rN4E...",
"slot": 287654321,
"timestamp": "2026-03-16T12:00:00Z",
"data": {
"agentPda": "BKq8rN4E...",
"owner": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"previousTier": "Basic",
"newTier": "Standard",
"stakeAmount": 1000000000
}
}
The request includes the following headers for verification and routing:
| Header | Description |
|---|---|
| X-Vouch-Signature | HMAC-SHA256 hex digest of <timestamp>.<payload> |
| X-Vouch-Timestamp | Unix timestamp (seconds) used in the signature |
| X-Vouch-Event | The event type (e.g. agent.tier_updated) |
Step 4 — Verify Webhook Signatures
Always verify the HMAC signature before processing a webhook. This ensures the payload was sent by Vouch and has not been tampered with.
import { createHmac, timingSafeEqual } from 'crypto'; // Node crypto for HMAC verification function verifyWebhookSignature( payload: string, // Raw JSON body signature: string, // X-Vouch-Signature header timestamp: string, // X-Vouch-Timestamp header secret: string, // Your stored webhook secret ): boolean { const signedContent = `${timestamp}.${payload}`; // Must match server's signing format const expected = createHmac('sha256', secret) // Compute HMAC using your webhook secret .update(signedContent) .digest('hex'); const a = Buffer.from(signature); // Signature from X-Vouch-Signature header const b = Buffer.from(expected); // Our computed expected signature if (a.length !== b.length) return false; // Reject if lengths differ (prevents timing attacks) return timingSafeEqual(a, b); // Constant-time comparison to prevent timing side-channels }
Step 5 — Manage Subscriptions
List Subscriptions
const headers = createAuthHeaders(wallet, 'GET', '/webhooks'); // Auth for listing subscriptions const res = await fetch('https://api.vouchprotocol.xyz/webhooks', { headers }); // GET all your webhook subscriptions const webhooks = await res.json();
{
"subscriptions": [
{
"id": "wh_abc123",
"url": "https://your-server.com/webhooks/vouch",
"eventTypes": ["agent.tier_updated", "agent.reputation_updated", "agent.status_changed"],
"active": true
}
]
}
Update a Subscription
const webhookId = 'wh_abc123'; // The webhook subscription ID to update const headers = createAuthHeaders(wallet, 'PATCH', `/webhooks/${webhookId}`); await fetch(`https://api.vouchprotocol.xyz/webhooks/${webhookId}`, { // PATCH to update event types method: 'PATCH', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ eventTypes: ['agent.tier_updated', 'agent.registered'], // New set of subscribed events }), });
Delete a Subscription
const webhookId = 'wh_abc123'; // The webhook subscription ID to delete const headers = createAuthHeaders(wallet, 'DELETE', `/webhooks/${webhookId}`); const res = await fetch(`https://api.vouchprotocol.xyz/webhooks/${webhookId}`, { // DELETE to unsubscribe method: 'DELETE', headers, }); const result = await res.json(); console.log('Deleted:', result.deleted); // true if the subscription was removed
Rotate Secret
const headers = createAuthHeaders(wallet, 'POST', `/webhooks/${webhookId}/rotate-secret`); const res = await fetch( // Generate a new HMAC secret, invalidating the old one `https://api.vouchprotocol.xyz/webhooks/${webhookId}/rotate-secret`, { method: 'POST', headers }, ); const { secret } = await res.json(); // New secret — update your server config immediately // Save the new secret!
Check Delivery Log
const headers = createAuthHeaders(wallet, 'GET', `/webhooks/${webhookId}/deliveries`); const res = await fetch( // Fetch delivery history to debug missed events `https://api.vouchprotocol.xyz/webhooks/${webhookId}/deliveries`, { headers }, ); const deliveries = await res.json(); // Contains status codes, timestamps, and retry info console.log('Recent deliveries:', deliveries);
Complete Working Example
A Node.js Express server that receives and verifies Vouch webhooks:
import express from 'express'; // Express web framework import { createHmac, timingSafeEqual } from 'crypto'; // HMAC verification const app = express(); const WEBHOOK_SECRET = process.env.VOUCH_WEBHOOK_SECRET || ''; // Secret from webhook creation response // Parse raw body for signature verification app.use('/webhooks/vouch', express.raw({ type: 'application/json' })); // Raw body needed for HMAC computation function verifySignature(payload: Buffer, signature: string, timestamp: string): boolean { const signedContent = `${timestamp}.${payload.toString()}`; // Timestamp + payload as signed by Vouch const expected = createHmac('sha256', WEBHOOK_SECRET) .update(signedContent) .digest('hex'); const a = Buffer.from(signature); const b = Buffer.from(expected); if (a.length !== b.length) return false; return timingSafeEqual(a, b); // Constant-time comparison } app.post('/webhooks/vouch', (req, res) => { // Endpoint that receives Vouch webhook events const signature = req.headers['x-vouch-signature'] as string; // HMAC signature from Vouch const timestamp = req.headers['x-vouch-timestamp'] as string; // Delivery timestamp from Vouch if (!signature || !timestamp || !verifySignature(req.body, signature, timestamp)) { // Always verify before processing console.error('Invalid webhook signature'); return res.status(401).send('Unauthorized'); } const event = JSON.parse(req.body.toString()); // Parse the verified payload switch (event.type) { // Route events to the appropriate handler case 'agent.tier_updated': console.log( `Agent ${event.data.agentPda} tier: ` + `${event.data.previousTier} -> ${event.data.newTier}` ); break; case 'agent.reputation_updated': console.log( `Agent ${event.data.agentPda} ` + `reputation: ${event.data.newReputation}` ); break; case 'agent.status_changed': console.log( `Agent ${event.data.agentPda} ` + `status: ${event.data.newStatus}` ); break; default: console.log('Unhandled event:', event.type); } res.status(200).send('OK'); // Respond 200 so Vouch knows delivery succeeded }); app.listen(3000, () => { console.log('Webhook receiver listening on port 3000'); });
Next Steps
- Webhooks API reference for all endpoint details
- Wallet Auth reference for authentication details
- Register an agent to start receiving events