Skip to main content

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 user
  • ad.click - An ad was clicked by a user
  • ad.conversion - A conversion was tracked for an ad
  • campaign.created - A new campaign was created
  • campaign.updated - A campaign was modified
  • campaign.paused - A campaign was paused
  • campaign.budget_exhausted - A campaign's budget was fully spent
  • account.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

  1. Always verify signatures to ensure webhooks come from Cue
  2. Use HTTPS for your webhook endpoints
  3. Return 200 status codes quickly to acknowledge receipt
  4. Process webhooks asynchronously for better performance
  5. Implement idempotency to handle duplicate events
  6. 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

  1. Webhook not receiving events: Check URL accessibility and HTTPS
  2. Signature verification fails: Ensure correct secret and signature handling
  3. Timeouts: Webhook endpoints must respond within 10 seconds
  4. 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