Webhooks and Observability for MCP Billing
Your MCP server is live, customers are paying, keys are being validated. Now you need to know when things happen — in real time. Here's how to wire up webhooks and observability.
Two layers of observability
Vend gives you two complementary ways to monitor your MCP billing:
- Webhooks — Server-to-server HTTP notifications when billing events happen (key created, subscription cancelled, usage limit reached). These fire from Vend's servers to your endpoint.
- SDK hooks — In-process callbacks that fire inside your MCP server when the SDK validates keys, records usage, or encounters errors. These run in your process, not over the network.
Webhooks are for reacting to billing events. SDK hooks are for operational observability. Most production setups use both.
Webhooks
Available events
Vend fires webhooks for these events:
key.created— A new license key was generated (via checkout or manually).key.activated— A key was activated on a new MCP client instance.key.deactivated— A key activation was removed.key.reactivated— A previously deactivated key was reactivated.subscription.cancelled— A customer cancelled their subscription.usage.limit_reached— A key hit its daily request limit.
Webhook payload
Every webhook POST includes a JSON body with the event type and relevant data:
{
"event": "key.created",
"data": {
"key_id": "uuid",
"project_id": "uuid",
"customer_email": "user@example.com",
"tier": "pro",
"created_at": "2026-04-10T12:00:00Z"
}
}Verifying signatures
Every webhook includes an x-vend-signature header containing an HMAC-SHA256 signature of the request body, signed with your webhook secret. Always verify this before processing:
import crypto from 'crypto';
function verifyWebhook(body: string, signature: string, secret: string) {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}Use timingSafeEqual to prevent timing attacks. Never compare signatures with ===.
Retry behavior
If your endpoint returns a non-2xx status, Vend retries up to 3 times with exponential backoff. This means your webhook handler should be idempotent — processing the same event twice should be safe.
SDK hooks
SDK hooks fire inside your MCP server process. They're callbacks you pass to vendAuth():
const guard = vendAuth({
apiKey: process.env.VEND_API_KEY!,
gates: { /* ... */ },
onValidate(result, fromCache) {
// Fires on every key validation
// result.valid, result.tier, result.reason
// fromCache: true if served from cache
},
onActivate(result) {
// Fires when a key is activated on this instance
// result.success, result.activationId
},
onUsage(toolName, success) {
// Fires after every recordUsage() call
// toolName: which tool was invoked
// success: whether recording succeeded
},
onError(operation, error) {
// Fires on any SDK error
// operation: 'validate' | 'activate' | 'usage'
// error: the Error object
},
});Practical patterns
Send key events to Slack
Set up a webhook endpoint that forwards events to a Slack channel. This gives your team instant visibility into new customers and cancellations:
// POST /api/vend-webhook
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get('x-vend-signature')!;
if (!verifyWebhook(body, sig, process.env.VEND_WEBHOOK_SECRET!)) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
const messages: Record<string, string> = {
'key.created': `New customer: ${event.data.customer_email} (${event.data.tier})`,
'subscription.cancelled': `Churn: ${event.data.customer_email} cancelled`,
'usage.limit_reached': `${event.data.customer_email} hit their limit`,
};
const message = messages[event.event];
if (message) {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
body: JSON.stringify({ text: message }),
});
}
return new Response('OK');
}Push metrics to Datadog / Grafana
Use SDK hooks to emit custom metrics. This lets you monitor validation latency, cache hit rates, and error rates alongside your existing infrastructure metrics:
import { StatsD } from 'hot-shots';
const statsd = new StatsD();
const guard = vendAuth({
apiKey: process.env.VEND_API_KEY!,
gates: { /* ... */ },
onValidate(result, fromCache) {
statsd.increment('vend.validation', {
valid: String(result.valid),
cached: String(fromCache),
tier: result.tier || 'none',
});
},
onUsage(toolName, success) {
statsd.increment('vend.usage', {
tool: toolName,
success: String(success),
});
},
onError(op, err) {
statsd.increment('vend.error', { operation: op });
},
});Alert on unusual patterns
Combine webhook events to detect patterns that matter:
- Activation spike — Multiple
key.activatedevents for the same key in a short window could indicate key sharing. - Rapid cancellations — A cluster of
subscription.cancelledevents might mean you shipped a breaking change. - Limit storms — Many
usage.limit_reachedevents could mean your rate limits are too low for how users actually use the tool.
Dashboard vs. hooks
The Vend dashboard shows you aggregated data: top tools, revenue totals, customer lists. Webhooks and SDK hooks give you real-time, event-level data. Use the dashboard for business questions (“how's revenue trending?”) and hooks for operational questions (“is validation failing right now?”).
Neither replaces the other. A mature MCP server uses all three: the dashboard for high-level metrics, webhooks for external integrations, and SDK hooks for in-process monitoring.
Ready to add observability to your MCP billing? Read the full webhook docs or create a project and start integrating.