Tutorial

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:

Terminal
npm install express crypto

Available Event Types

Fetch the list of available events from the public endpoint:

cURL
# Fetch the list of event types you can subscribe to
curl https://api.vouchprotocol.xyz/webhooks/event-types
Response
{
  "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

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
TypeScript — Create Auth Headers
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

TypeScript
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.
Response
{
  "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:

Webhook 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.

TypeScript
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

TypeScript
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();
Response
{
  "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

TypeScript
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

TypeScript
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

TypeScript
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

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

webhook-server.ts
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