Webhooks
Webhooks push real-time notifications to your server when events occur in Vantage. Instead of polling for changes, get instant updates when invoices are paid, tickets are created, or time is logged.
How Webhooks Work
- You register a webhook endpoint URL
- You select which events to subscribe to
- When an event occurs, Vantage sends a POST request to your URL
- Your server acknowledges receipt with a 2xx response
Vantage Event → POST to your URL → Your server processes → Returns 200 OKSetting Up Webhooks
In the Dashboard
- Go to Settings → API → Webhooks
- Click Add Webhook
- Enter your endpoint URL
- Select events to subscribe to
- Copy your webhook secret (for signature verification)
- Click Save
Via API
/v1/webhooksCreate a webhookcurl -X POST "https://api.govantage.co/v1/webhooks" \
-H "Authorization: Bearer vnt_sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/vantage",
"events": ["invoice.paid", "ticket.created", "time_entry.created"],
"description": "Production webhook"
}'Response
{
"data": {
"id": "whk_cuid123456",
"url": "https://yourapp.com/webhooks/vantage",
"events": ["invoice.paid", "ticket.created", "time_entry.created"],
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "active",
"created_at": "2026-01-25T10:30:00Z"
}
}The webhook secret is only shown once. Store it securely—you’ll need it to verify signatures.
Webhook Payload
All webhook deliveries include:
{
"id": "evt_cuid123456",
"type": "invoice.paid",
"created_at": "2026-01-25T14:30:00Z",
"workspace_id": "ws_xxxxx",
"data": {
"id": "inv_xxxxx",
"invoice_number": "INV-2026-0042",
"client_id": "cli_xxxxx",
"total": 5412.50,
"paid_date": "2026-01-25"
}
}Payload Fields
| Field | Description |
|---|---|
id | Unique event ID (for deduplication) |
type | Event type (e.g., invoice.paid) |
created_at | When the event occurred |
workspace_id | Your workspace ID |
data | Event-specific data (the affected object) |
Verifying Signatures
Every webhook includes a signature header for verification. Always verify signatures to ensure the webhook came from Vantage.
Signature Header
X-Vantage-Signature: t=1706191800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdThe header contains:
t- Timestamp (Unix seconds)v1- HMAC-SHA256 signature
Verification Steps
- Extract timestamp and signature from header
- Build the signed payload:
{timestamp}.{request_body} - Compute HMAC-SHA256 using your webhook secret
- Compare signatures (constant-time comparison)
- Check timestamp is within 5 minutes (prevents replay attacks)
Example Code
const crypto = require('crypto');
function verifyWebhook(payload, header, secret) {
const [tPart, sigPart] = header.split(',');
const timestamp = tPart.split('=')[1];
const signature = sigPart.split('=')[1];
// Check timestamp (5 min tolerance)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
throw new Error('Webhook timestamp too old');
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid webhook signature');
}
return JSON.parse(payload);
}
// Express.js example
app.post('/webhooks/vantage', express.raw({type: 'application/json'}), (req, res) => {
try {
const event = verifyWebhook(
req.body.toString(),
req.headers['x-vantage-signature'],
process.env.VANTAGE_WEBHOOK_SECRET
);
// Handle the event
switch (event.type) {
case 'invoice.paid':
handleInvoicePaid(event.data);
break;
// ... other cases
}
res.status(200).send('OK');
} catch (err) {
res.status(400).send(err.message);
}
});Available Events
Invoice Events
| Event | Description |
|---|---|
invoice.created | Invoice created |
invoice.updated | Invoice details changed |
invoice.sent | Invoice emailed to client |
invoice.viewed | Client viewed invoice |
invoice.paid | Invoice fully paid |
invoice.partial_payment | Partial payment received |
invoice.overdue | Invoice became overdue |
invoice.voided | Invoice voided |
Client Events
| Event | Description |
|---|---|
client.created | New client created |
client.updated | Client details changed |
client.deleted | Client deleted |
client.health_changed | Health score crossed threshold |
Project Events
| Event | Description |
|---|---|
project.created | New project created |
project.updated | Project details changed |
project.status_changed | Status transitioned |
project.budget_alert | Budget threshold crossed (75%, 90%, 100%) |
project.completed | Project marked complete |
Ticket Events
| Event | Description |
|---|---|
ticket.created | New ticket created |
ticket.updated | Ticket details changed |
ticket.assigned | Ticket assigned/reassigned |
ticket.status_changed | Status transitioned |
ticket.commented | New comment/activity added |
ticket.closed | Ticket closed |
ticket.reopened | Ticket reopened |
ticket.sla_warning | SLA breach approaching (80%) |
ticket.sla_breached | SLA breached |
Time Entry Events
| Event | Description |
|---|---|
time_entry.created | Time logged |
time_entry.updated | Time entry modified |
time_entry.deleted | Time entry deleted |
time_entry.approved | Time entry approved |
time_entry.rejected | Time entry rejected |
Agreement Events
| Event | Description |
|---|---|
agreement.created | New retainer created |
agreement.updated | Agreement details changed |
agreement.period_closed | Period ended |
agreement.near_limit | 90% of hours used |
agreement.over_limit | Hours exceeded allowance |
agreement.expiring_soon | Expiring within 30 days |
User Events
| Event | Description |
|---|---|
user.created | New team member added |
user.updated | User details changed |
user.deactivated | User deactivated |
Retry Policy
If your endpoint doesn’t respond with 2xx within 30 seconds, Vantage retries:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7 | 24 hours |
After 7 failed attempts, the webhook is marked as failed. You’ll receive an email notification.
Use the event id field for idempotency. The same event may be delivered multiple times during retries.
Managing Webhooks
List Webhooks
/v1/webhooksList all webhooksUpdate Webhook
/v1/webhooks/:idUpdate webhookcurl -X PUT "https://api.govantage.co/v1/webhooks/whk_xxxxx" \
-H "Authorization: Bearer vnt_sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"events": ["invoice.paid", "invoice.created"],
"status": "active"
}'Delete Webhook
/v1/webhooks/:idDelete webhookRotate Secret
/v1/webhooks/:id/rotate-secretGenerate new secretcurl -X POST "https://api.govantage.co/v1/webhooks/whk_xxxxx/rotate-secret" \
-H "Authorization: Bearer vnt_sk_live_xxxxx"Testing Webhooks
Send Test Event
/v1/webhooks/:id/testSend a test eventcurl -X POST "https://api.govantage.co/v1/webhooks/whk_xxxxx/test" \
-H "Authorization: Bearer vnt_sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"event_type": "invoice.paid"
}'View Delivery History
/v1/webhooks/:id/deliveriesList recent deliveries{
"data": [
{
"id": "del_xxxxx",
"event_id": "evt_xxxxx",
"event_type": "invoice.paid",
"status": "delivered",
"response_code": 200,
"response_time_ms": 145,
"delivered_at": "2026-01-25T14:30:00Z"
},
{
"id": "del_yyyyy",
"event_id": "evt_yyyyy",
"event_type": "ticket.created",
"status": "failed",
"response_code": 500,
"error": "Internal Server Error",
"next_retry_at": "2026-01-25T14:35:00Z"
}
]
}Best Practices
Do
- Respond quickly - Return 200 immediately, process async
- Verify signatures - Always validate webhook authenticity
- Handle duplicates - Use event ID for idempotency
- Log everything - Keep records for debugging
- Use HTTPS - Webhooks only deliver to secure endpoints
Don’t
- Block the response - Don’t do heavy processing before responding
- Ignore failures - Monitor your webhook health
- Hardcode secrets - Use environment variables
- Trust the payload - Validate data before acting on it
Example: Async Processing
app.post('/webhooks/vantage', async (req, res) => {
// Verify signature
const event = verifyWebhook(req.body, req.headers, secret);
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
await queue.add('process-webhook', { event });
});Troubleshooting
Webhooks Not Arriving
- Check webhook status is
active - Verify endpoint URL is correct and accessible
- Check your server logs for incoming requests
- Ensure firewall allows Vantage IPs
Signature Verification Failing
- Use the raw request body (not parsed JSON)
- Check you’re using the correct secret
- Verify timestamp tolerance (5 minutes)
- Ensure no middleware is modifying the body
Frequent Retries
- Return 200 faster (process async)
- Check your endpoint isn’t timing out
- Ensure your server can handle the load
Webhook IPs
If you need to allowlist Vantage IPs:
52.xx.xx.xx
52.xx.xx.xx
52.xx.xx.xxIPs may change. We recommend verifying signatures instead of IP allowlisting.
Next Steps
- Authentication - API keys and security
- Error Handling - Handle webhook errors
- Best Practices - API usage patterns