Skip to Content

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

  1. Go to SettingsWebhooks
  2. Click Add Webhook
  3. Enter your endpoint URL
  4. Select events to receive
  5. 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 SettingsWebhooks → 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/aispendtrack

Test Events

Send test events from dashboard:

  1. Go to SettingsWebhooks
  2. Click your webhook
  3. Click Send Test Event
  4. Choose event type
  5. Click Send

View Webhook Logs

See delivery history:

  1. Go to SettingsWebhooks
  2. Click your webhook
  3. Click Delivery Log
  4. See all attempts, status codes, response times

Webhook Limits

TierMax WebhooksEvents/Day
Pro510,000
EnterpriseUnlimitedUnlimited

Troubleshooting

Webhooks not being received?

  1. Check endpoint is HTTPS (required)
  2. Verify firewall allows incoming requests
  3. Check delivery log for error messages
  4. Send test event to verify setup
  5. 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 } }) }); } });

Next Steps

Last updated on