Webhooks
Receive real-time notifications about your AI costs.
Webhooks are available on Pro and Enterprise tiers.
Overview
Webhooks let you receive HTTP POST requests when events occur:
- Cost thresholds exceeded
- Daily/weekly/monthly summaries
- Anomalies detected
- Errors or downtime
Setting Up Webhooks
1. Create Webhook Endpoint
Create an HTTPS endpoint that accepts POST requests:
// Express.js example
app.post('/webhooks/aispendtrack', (req, res) => {
const event = req.body;
// Verify signature (recommended)
const signature = req.headers['x-aispendtrack-signature'];
if (!verifySignature(signature, req.body)) {
return res.status(401).send('Invalid signature');
}
// Process event
console.log('Event received:', event.type);
// Respond quickly (process async)
res.status(200).send('OK');
// Process event asynchronously
processEvent(event);
});2. Add Webhook in Dashboard
- Go to Settings → Webhooks
- Click Add Webhook
- Enter your endpoint URL
- Select events to receive
- Save
3. Test Webhook
Click Send Test Event to verify setup.
Event Types
cost.threshold_exceeded
Triggered when spending exceeds a threshold.
Payload:
{
"id": "evt_abc123",
"type": "cost.threshold_exceeded",
"created": 1708272000,
"data": {
"threshold": 100.00,
"current": 105.50,
"period": "daily",
"model": "gpt-4",
"timestamp": "2024-02-18T14:30:00Z"
}
}cost.daily_summary
Daily cost summary (sent at midnight UTC).
Payload:
{
"id": "evt_def456",
"type": "cost.daily_summary",
"created": 1708272000,
"data": {
"date": "2024-02-18",
"total_cost": 45.50,
"total_calls": 1250,
"top_model": "gpt-4",
"top_customer": "user_123",
"breakdown": {
"gpt-4": 30.00,
"gpt-3.5-turbo": 15.50
}
}
}cost.weekly_summary
Weekly cost summary (sent Sunday midnight UTC).
Payload:
{
"id": "evt_ghi789",
"type": "cost.weekly_summary",
"created": 1708272000,
"data": {
"week_start": "2024-02-12",
"week_end": "2024-02-18",
"total_cost": 320.50,
"total_calls": 8750,
"avg_daily_cost": 45.79,
"trend": "increasing",
"top_models": ["gpt-4", "claude-3-opus"],
"anomalies": []
}
}cost.anomaly_detected
Unusual cost spike detected.
Payload:
{
"id": "evt_jkl012",
"type": "cost.anomaly_detected",
"created": 1708272000,
"data": {
"severity": "high",
"current_rate": 10.50,
"normal_rate": 2.30,
"multiplier": 4.6,
"duration": "last_hour",
"model": "gpt-4",
"customer": "user_456"
}
}usage.limit_approaching
Approaching monthly usage limit.
Payload:
{
"id": "evt_mno345",
"type": "usage.limit_approaching",
"created": 1708272000,
"data": {
"current": 9500,
"limit": 10000,
"percentage": 95,
"remaining": 500,
"reset_date": "2024-03-01"
}
}error.rate_high
Error rate is unusually high.
Payload:
{
"id": "evt_pqr678",
"type": "error.rate_high",
"created": 1708272000,
"data": {
"error_rate": 5.2,
"normal_rate": 0.5,
"period": "last_hour",
"top_error": "rate_limit_exceeded",
"affected_models": ["gpt-4"]
}
}Webhook Signatures
Verify webhook authenticity:
Signature Verification
AiSpendTrack signs webhooks using HMAC SHA256.
Header:
x-aispendtrack-signature: t=1708272000,v1=abc123def456...Verify in Node.js:
const crypto = require('crypto');
function verifySignature(signature, payload) {
const [timestamp, hash] = signature.split(',').map(p => p.split('=')[1]);
// Reject old signatures (>5 min)
if (Date.now() / 1000 - timestamp > 300) {
return false;
}
// Compute expected signature
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const expectedHash = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
// Compare
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(expectedHash)
);
}Webhook secret: Find in Settings → Webhooks → Your webhook → Signing secret
Always verify signatures in production to prevent unauthorized requests.
Retry Logic
If webhook delivery fails:
- Retry after 1 minute
- Retry after 5 minutes
- Retry after 15 minutes
- Retry after 1 hour
- Give up after 4 failures
Failed delivery email: After 4 failures, we email you at your account email.
Best Practices
1. Respond Quickly
Respond with 200 OK within 5 seconds:
app.post('/webhook', async (req, res) => {
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
processWebhook(req.body).catch(console.error);
});2. Handle Duplicates
Webhooks may be delivered more than once:
const processedEvents = new Set();
async function processWebhook(event) {
// Skip if already processed
if (processedEvents.has(event.id)) {
return;
}
// Process
await handleEvent(event);
// Mark as processed
processedEvents.add(event.id);
}3. Verify Signatures
Always verify signatures in production:
if (!verifySignature(req.headers['x-aispendtrack-signature'], req.body)) {
return res.status(401).send('Invalid signature');
}4. Use HTTPS
Webhook URLs must use HTTPS in production.
5. Handle Errors Gracefully
Don’t throw errors that cause 500 responses:
try {
await processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing failed:', error);
res.status(200).send('OK'); // Still return 200 to prevent retries
}Testing Webhooks
Local Testing
Use ngrok or similar to expose localhost:
# Install ngrok
npm install -g ngrok
# Expose port 3000
ngrok http 3000
# Use ngrok URL in webhook settings
# https://abc123.ngrok.io/webhooks/aispendtrackTest Events
Send test events from dashboard:
- Go to Settings → Webhooks
- Click your webhook
- Click Send Test Event
- Choose event type
- Click Send
View Webhook Logs
See delivery history:
- Go to Settings → Webhooks
- Click your webhook
- Click Delivery Log
- See all attempts, status codes, response times
Webhook Limits
| Tier | Max Webhooks | Events/Day |
|---|---|---|
| Pro | 5 | 10,000 |
| Enterprise | Unlimited | Unlimited |
Troubleshooting
Webhooks not being received?
- Check endpoint is HTTPS (required)
- Verify firewall allows incoming requests
- Check delivery log for error messages
- Send test event to verify setup
- Check your server logs for incoming requests
Getting 401 errors?
- Verify signature correctly
- Check webhook secret is correct
- Ensure timestamp is within 5 minutes
Getting timeouts?
- Respond within 5 seconds
- Process events asynchronously
- Return 200 immediately
Example Implementations
Slack Notifications
app.post('/webhooks/aispendtrack', async (req, res) => {
res.status(200).send('OK');
const event = req.body;
if (event.type === 'cost.threshold_exceeded') {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🚨 Cost Alert: Exceeded $${event.data.threshold}`,
attachments: [{
color: 'danger',
fields: [
{ title: 'Current', value: `$${event.data.current}`, short: true },
{ title: 'Limit', value: `$${event.data.threshold}`, short: true },
{ title: 'Model', value: event.data.model, short: true }
]
}]
})
});
}
});Database Logging
app.post('/webhooks/aispendtrack', async (req, res) => {
res.status(200).send('OK');
// Log all events to database
await db.webhookEvents.create({
event_id: req.body.id,
type: req.body.type,
data: req.body.data,
received_at: new Date()
});
});PagerDuty Integration
app.post('/webhooks/aispendtrack', async (req, res) => {
res.status(200).send('OK');
if (req.body.type === 'cost.anomaly_detected' &&
req.body.data.severity === 'high') {
// Create PagerDuty incident
await fetch('https://events.pagerduty.com/v2/enqueue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Token token=${process.env.PAGERDUTY_KEY}`
},
body: JSON.stringify({
event_action: 'trigger',
payload: {
summary: 'AI cost anomaly detected',
severity: 'critical',
source: 'AiSpendTrack',
custom_details: req.body.data
}
})
});
}
});