Webhooks
Cue webhooks allow you to receive real-time notifications about events in your account, such as campaign performance updates, ad interactions, and billing events.
Webhook Overview
Webhooks are HTTP callbacks that Cue sends to your application when specific events occur. Instead of polling our API for updates, you can set up webhooks to receive instant notifications.
Supported Events
ad.impression- An ad was displayed to a userad.click- An ad was clicked by a userad.conversion- A conversion was tracked for an adcampaign.created- A new campaign was createdcampaign.updated- A campaign was modifiedcampaign.paused- A campaign was pausedcampaign.budget_exhausted- A campaign's budget was fully spentaccount.billing- Billing events (payments, invoices)
Setting Up Webhooks
1. Create a Webhook Endpoint
Create an HTTP endpoint in your application to receive webhook events:
// Express.js example
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/cue', (req, res) => {
const signature = req.headers['cue-signature'];
const body = req.body;
// Verify webhook signature (see security section)
if (!verifyWebhookSignature(body, signature)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(body);
// Handle the event
handleWebhookEvent(event);
res.status(200).send('OK');
});
function handleWebhookEvent(event) {
switch (event.type) {
case 'ad.click':
console.log('Ad clicked:', event.data);
break;
case 'campaign.budget_exhausted':
console.log('Campaign budget exhausted:', event.data);
break;
default:
console.log('Unhandled event type:', event.type);
}
}
2. Register Your Webhook
Use the Cue API to register your webhook endpoint:
curl -X POST https://api.oncue.ad/api/v1/webhooks \
-H "Content-Type: application/json" \
-H "x-user-id: your-user-id" \
-d '{
"url": "https://your-app.com/webhooks/cue",
"events": ["ad.click", "ad.impression", "campaign.budget_exhausted"],
"description": "Main webhook endpoint"
}'
Webhook Payload Structure
All webhook events follow this structure:
{
"id": "evt_1234567890",
"type": "ad.click",
"created": "2024-01-15T10:30:00Z",
"data": {
// Event-specific data
},
"api_version": "v1"
}
Event Types and Payloads
Ad Impression
Triggered when an ad is displayed to a user.
{
"id": "evt_1234567890",
"type": "ad.impression",
"created": "2024-01-15T10:30:00Z",
"data": {
"ad_id": "ad-123",
"campaign_id": "campaign-456",
"user_id": "user-789",
"session_id": "session-abc",
"context": {
"conversation": "User asking about project management tools",
"user_query": "What are good options for small teams?"
},
"placement": {
"type": "inline",
"position": "after_response"
},
"metadata": {
"user_agent": "Mozilla/5.0...",
"ip_address": "192.168.1.1",
"timestamp": "2024-01-15T10:30:00Z"
}
}
}
Ad Click
Triggered when a user clicks on an ad.
{
"id": "evt_1234567891",
"type": "ad.click",
"created": "2024-01-15T10:35:00Z",
"data": {
"ad_id": "ad-123",
"campaign_id": "campaign-456",
"click_id": "click-xyz",
"user_id": "user-789",
"session_id": "session-abc",
"landing_url": "https://example.com/signup?ref=cue",
"context": {
"conversation": "User asking about project management tools",
"user_query": "What are good options for small teams?"
},
"metadata": {
"user_agent": "Mozilla/5.0...",
"ip_address": "192.168.1.1",
"timestamp": "2024-01-15T10:35:00Z"
}
}
}
Ad Conversion
Triggered when a conversion is tracked for an ad.
{
"id": "evt_1234567892",
"type": "ad.conversion",
"created": "2024-01-15T11:00:00Z",
"data": {
"ad_id": "ad-123",
"campaign_id": "campaign-456",
"click_id": "click-xyz",
"conversion_id": "conv-def",
"user_id": "user-789",
"conversion_type": "signup",
"conversion_value": 99.99,
"currency": "USD",
"metadata": {
"timestamp": "2024-01-15T11:00:00Z"
}
}
}
Campaign Budget Exhausted
Triggered when a campaign's budget is fully spent.
{
"id": "evt_1234567893",
"type": "campaign.budget_exhausted",
"created": "2024-01-15T12:00:00Z",
"data": {
"campaign_id": "campaign-456",
"campaign_name": "Tech Product Launch",
"budget": 10000,
"spent": 10000,
"performance": {
"impressions": 25000,
"clicks": 500,
"conversions": 45,
"ctr": 2.0,
"average_cpb": 20.0
}
}
}
Campaign Created
Triggered when a new campaign is created.
{
"id": "evt_1234567894",
"type": "campaign.created",
"created": "2024-01-15T09:00:00Z",
"data": {
"campaign_id": "campaign-789",
"campaign_name": "New Brand Campaign",
"budget": 5000,
"daily_budget": 200,
"status": "active",
"target_audience": {
"keywords": ["technology", "innovation"],
"demographics": {
"age_range": "25-45"
}
}
}
}
Security
Webhook Signature Verification
Cue signs each webhook payload with a secret key. Verify the signature to ensure the webhook came from Cue:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
const actualSignature = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(actualSignature, 'hex')
);
}
// Usage
const isValid = verifyWebhookSignature(
req.body,
req.headers['cue-signature'],
process.env.CUE_WEBHOOK_SECRET
);
import hmac
import hashlib
def verify_webhook_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
actual_signature = signature.replace('sha256=', '')
return hmac.compare_digest(expected_signature, actual_signature)
# Usage
is_valid = verify_webhook_signature(
request.body,
request.headers['cue-signature'],
os.environ['CUE_WEBHOOK_SECRET']
)
Best Practices
- Always verify signatures to ensure webhooks come from Cue
- Use HTTPS for your webhook endpoints
- Return 200 status codes quickly to acknowledge receipt
- Process webhooks asynchronously for better performance
- Implement idempotency to handle duplicate events
- Log webhook events for debugging and monitoring
Webhook Management API
List Webhooks
curl -X GET https://api.oncue.ad/api/v1/webhooks \
-H "x-user-id: your-user-id"
Create Webhook
curl -X POST https://api.oncue.ad/api/v1/webhooks \
-H "Content-Type: application/json" \
-H "x-user-id: your-user-id" \
-d '{
"url": "https://your-app.com/webhooks/cue",
"events": ["ad.click", "ad.impression"],
"description": "Main webhook endpoint"
}'
Update Webhook
curl -X PATCH https://api.oncue.ad/api/v1/webhooks/webhook-123 \
-H "Content-Type: application/json" \
-H "x-user-id: your-user-id" \
-d '{
"events": ["ad.click", "ad.impression", "campaign.budget_exhausted"]
}'
Delete Webhook
curl -X DELETE https://api.oncue.ad/api/v1/webhooks/webhook-123 \
-H "x-user-id: your-user-id"
Test Webhook
curl -X POST https://api.oncue.ad/api/v1/webhooks/webhook-123/test \
-H "x-user-id: your-user-id"
Handling Webhook Failures
Retry Logic
Cue automatically retries failed webhooks with exponential backoff:
- Initial retry: 1 second
- Second retry: 4 seconds
- Third retry: 16 seconds
- Fourth retry: 64 seconds
- Fifth retry: 256 seconds
After 5 failed attempts, the webhook is marked as failed and retries stop.
Monitoring
Monitor your webhook endpoint health:
const webhookStats = {
received: 0,
processed: 0,
failed: 0,
lastProcessed: null
};
app.post('/webhooks/cue', (req, res) => {
webhookStats.received++;
try {
const event = JSON.parse(req.body);
handleWebhookEvent(event);
webhookStats.processed++;
webhookStats.lastProcessed = new Date();
res.status(200).send('OK');
} catch (error) {
webhookStats.failed++;
console.error('Webhook processing failed:', error);
res.status(500).send('Error');
}
});
// Health check endpoint
app.get('/webhooks/health', (req, res) => {
res.json(webhookStats);
});
Example Implementations
Node.js with Express
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.raw({ type: 'application/json' }));
class WebhookHandler {
constructor(secret) {
this.secret = secret;
}
verifySignature(payload, signature) {
const expectedSignature = crypto
.createHmac('sha256', this.secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(signature.replace('sha256=', ''), 'hex')
);
}
async handleEvent(event) {
switch (event.type) {
case 'ad.impression':
await this.handleAdImpression(event.data);
break;
case 'ad.click':
await this.handleAdClick(event.data);
break;
case 'campaign.budget_exhausted':
await this.handleBudgetExhausted(event.data);
break;
default:
console.log('Unhandled event type:', event.type);
}
}
async handleAdImpression(data) {
// Track impression in your analytics
console.log('Ad impression:', data.ad_id);
}
async handleAdClick(data) {
// Track click conversion
console.log('Ad click:', data.ad_id, data.click_id);
}
async handleBudgetExhausted(data) {
// Send notification to advertiser
console.log('Budget exhausted for campaign:', data.campaign_id);
}
}
const webhookHandler = new WebhookHandler(process.env.CUE_WEBHOOK_SECRET);
app.post('/webhooks/cue', async (req, res) => {
const signature = req.headers['cue-signature'];
if (!webhookHandler.verifySignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}
try {
const event = JSON.parse(req.body);
await webhookHandler.handleEvent(event);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Error');
}
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
Python with Flask
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
app = Flask(__name__)
class WebhookHandler:
def __init__(self, secret):
self.secret = secret
def verify_signature(self, payload, signature):
expected_signature = hmac.new(
self.secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
actual_signature = signature.replace('sha256=', '')
return hmac.compare_digest(expected_signature, actual_signature)
async def handle_event(self, event):
event_type = event['type']
data = event['data']
if event_type == 'ad.impression':
await self.handle_ad_impression(data)
elif event_type == 'ad.click':
await self.handle_ad_click(data)
elif event_type == 'campaign.budget_exhausted':
await self.handle_budget_exhausted(data)
else:
print(f'Unhandled event type: {event_type}')
async def handle_ad_impression(self, data):
print(f'Ad impression: {data["ad_id"]}')
async def handle_ad_click(self, data):
print(f'Ad click: {data["ad_id"]}, {data["click_id"]}')
async def handle_budget_exhausted(self, data):
print(f'Budget exhausted for campaign: {data["campaign_id"]}')
webhook_handler = WebhookHandler(os.environ['CUE_WEBHOOK_SECRET'])
@app.route('/webhooks/cue', methods=['POST'])
def handle_webhook():
signature = request.headers.get('cue-signature')
payload = request.get_data(as_text=True)
if not webhook_handler.verify_signature(payload, signature):
return 'Invalid signature', 401
try:
event = json.loads(payload)
webhook_handler.handle_event(event)
return 'OK', 200
except Exception as e:
print(f'Webhook error: {e}')
return 'Error', 500
if __name__ == '__main__':
app.run(port=3000)
Testing Webhooks
Local Development
Use tools like ngrok to expose your local webhook endpoint:
# Install ngrok
npm install -g ngrok
# Expose your local server
ngrok http 3000
# Use the ngrok URL in your webhook configuration
# https://abc123.ngrok.io/webhooks/cue
Webhook Testing Tool
// test-webhook.js
const express = require('express');
const app = express();
app.use(express.json());
app.post('/test-webhook', (req, res) => {
console.log('Webhook received:');
console.log('Headers:', req.headers);
console.log('Body:', req.body);
res.status(200).send('OK');
});
app.listen(3000, () => {
console.log('Test webhook server running on port 3000');
});
Troubleshooting
Common Issues
- Webhook not receiving events: Check URL accessibility and HTTPS
- Signature verification fails: Ensure correct secret and signature handling
- Timeouts: Webhook endpoints must respond within 10 seconds
- Duplicate events: Implement idempotency using event IDs
Debug Webhook
app.post('/webhooks/cue', (req, res) => {
console.log('=== Webhook Debug ===');
console.log('Headers:', req.headers);
console.log('Body:', req.body.toString());
console.log('Signature:', req.headers['cue-signature']);
console.log('===================');
// Your webhook handling logic here
res.status(200).send('OK');
});
For webhook support: support@oncue.ad