Skip to main content
Skip to Content

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

  1. You register a webhook endpoint URL
  2. You select which events to subscribe to
  3. When an event occurs, Vantage sends a POST request to your URL
  4. Your server acknowledges receipt with a 2xx response
Vantage Event → POST to your URL → Your server processes → Returns 200 OK

Setting Up Webhooks

In the Dashboard

  1. Go to SettingsAPIWebhooks
  2. Click Add Webhook
  3. Enter your endpoint URL
  4. Select events to subscribe to
  5. Copy your webhook secret (for signature verification)
  6. Click Save

Via API

POST/v1/webhooksCreate a webhook
curl -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

FieldDescription
idUnique event ID (for deduplication)
typeEvent type (e.g., invoice.paid)
created_atWhen the event occurred
workspace_idYour workspace ID
dataEvent-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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

The header contains:

Verification Steps

  1. Extract timestamp and signature from header
  2. Build the signed payload: {timestamp}.{request_body}
  3. Compute HMAC-SHA256 using your webhook secret
  4. Compare signatures (constant-time comparison)
  5. 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

EventDescription
invoice.createdInvoice created
invoice.updatedInvoice details changed
invoice.sentInvoice emailed to client
invoice.viewedClient viewed invoice
invoice.paidInvoice fully paid
invoice.partial_paymentPartial payment received
invoice.overdueInvoice became overdue
invoice.voidedInvoice voided

Client Events

EventDescription
client.createdNew client created
client.updatedClient details changed
client.deletedClient deleted
client.health_changedHealth score crossed threshold

Project Events

EventDescription
project.createdNew project created
project.updatedProject details changed
project.status_changedStatus transitioned
project.budget_alertBudget threshold crossed (75%, 90%, 100%)
project.completedProject marked complete

Ticket Events

EventDescription
ticket.createdNew ticket created
ticket.updatedTicket details changed
ticket.assignedTicket assigned/reassigned
ticket.status_changedStatus transitioned
ticket.commentedNew comment/activity added
ticket.closedTicket closed
ticket.reopenedTicket reopened
ticket.sla_warningSLA breach approaching (80%)
ticket.sla_breachedSLA breached

Time Entry Events

EventDescription
time_entry.createdTime logged
time_entry.updatedTime entry modified
time_entry.deletedTime entry deleted
time_entry.approvedTime entry approved
time_entry.rejectedTime entry rejected

Agreement Events

EventDescription
agreement.createdNew retainer created
agreement.updatedAgreement details changed
agreement.period_closedPeriod ended
agreement.near_limit90% of hours used
agreement.over_limitHours exceeded allowance
agreement.expiring_soonExpiring within 30 days

User Events

EventDescription
user.createdNew team member added
user.updatedUser details changed
user.deactivatedUser deactivated

Retry Policy

If your endpoint doesn’t respond with 2xx within 30 seconds, Vantage retries:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
68 hours
724 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

GET/v1/webhooksList all webhooks

Update Webhook

PUT/v1/webhooks/:idUpdate webhook
curl -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

DELETE/v1/webhooks/:idDelete webhook

Rotate Secret

POST/v1/webhooks/:id/rotate-secretGenerate new secret
curl -X POST "https://api.govantage.co/v1/webhooks/whk_xxxxx/rotate-secret" \
  -H "Authorization: Bearer vnt_sk_live_xxxxx"

Testing Webhooks

Send Test Event

POST/v1/webhooks/:id/testSend a test event
curl -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

GET/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

Don’t

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

  1. Check webhook status is active
  2. Verify endpoint URL is correct and accessible
  3. Check your server logs for incoming requests
  4. Ensure firewall allows Vantage IPs

Signature Verification Failing

  1. Use the raw request body (not parsed JSON)
  2. Check you’re using the correct secret
  3. Verify timestamp tolerance (5 minutes)
  4. Ensure no middleware is modifying the body

Frequent Retries

  1. Return 200 faster (process async)
  2. Check your endpoint isn’t timing out
  3. 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.xx

IPs may change. We recommend verifying signatures instead of IP allowlisting.


Next Steps