Receive real-time notifications when campaigns, creatives, impressions, conversions, and billing events occur. All events use the CloudEvents 1.0 specification and are delivered via Kafka topics.
| Topic | Description | Trigger |
|---|---|---|
| eventzr.ads.campaign.created.v1 | A new campaign was created | POST /campaigns |
| eventzr.ads.campaign.launched.v1 | Campaign status changed to active | POST /campaigns/:id/launch |
| eventzr.ads.campaign.paused.v1 | Campaign was paused | POST /campaigns/:id/pause |
| eventzr.ads.campaign.resumed.v1 | Campaign was resumed | POST /campaigns/:id/resume |
| eventzr.ads.campaign.completed.v1 | Campaign reached its end date | Automatic (scheduler) |
| eventzr.ads.campaign.budget_exhausted.v1 | Campaign daily/total budget depleted | Automatic (billing) |
| eventzr.ads.creative.approved.v1 | Creative passed review | POST /creatives/:id/approve |
| eventzr.ads.creative.rejected.v1 | Creative was rejected | POST /creatives/:id/reject |
| eventzr.ads.impression.tracked.v1 | An ad impression was recorded | POST /impressions |
| eventzr.ads.click.tracked.v1 | An ad click was recorded | POST /clicks |
| eventzr.ads.conversion.tracked.v1 | A conversion event was tracked | POST /conversions |
| eventzr.ads.report.ready.v1 | Scheduled report is ready for download | Automatic (scheduler) |
| eventzr.ads.billing.low_balance.v1 | Ad account balance below threshold | Automatic (billing) |
| eventzr.ads.billing.payment_received.v1 | Funds were added to ad account | POST /billing/add-funds |
| eventzr.ads.audience.size_changed.v1 | Audience size crossed a threshold | Automatic (hourly check) |
| eventzr.ads.pixel.event_received.v1 | Retargeting pixel fired an event | Pixel JS (client-side) |
| eventzr.ads.campaign.deleted.v1 | Campaign was permanently deleted | DELETE /campaigns/:id |
| eventzr.ads.campaign.updated.v1 | Campaign settings were modified | PATCH /campaigns/:id |
All webhook payloads follow the CloudEvents 1.0 specification. The event metadata is in the envelope headers, and the domain-specific data is in the data field.
{
"specversion": "1.0",
"id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"source": "eventzr.ads-svc",
"type": "eventzr.ads.campaign.launched.v1",
"datacontenttype": "application/json",
"time": "2026-03-09T15:30:00.000Z",
"subject": "cmp_xyz789",
"tenantid": "tenant_abc123",
"data": {
"campaignId": "cmp_xyz789",
"name": "Summer Event Promo 2026",
"status": "active",
"previousStatus": "draft",
"launchedBy": "usr_admin456",
"launchedAt": "2026-03-09T15:30:00.000Z",
"budgetAmount": 5000,
"currency": "USD"
}
}| Field | Type | Description |
|---|---|---|
| specversion | string | Always "1.0" |
| id | string | Unique event ID (UUID v4) |
| source | string | Originating service ("eventzr.ads-svc") |
| type | string | Event type (Kafka topic name) |
| time | string | ISO 8601 timestamp of when the event occurred |
| subject | string | Primary entity ID (campaign, creative, etc.) |
| tenantid | string | Tenant UUID (Kafka partition key) |
| data | object | Domain-specific event payload |
{
"specversion": "1.0",
"id": "evt_conv_789xyz",
"source": "eventzr.ads-svc",
"type": "eventzr.ads.conversion.tracked.v1",
"time": "2026-03-09T16:45:00.000Z",
"subject": "cmp_xyz789",
"tenantid": "tenant_abc123",
"data": {
"conversionId": "conv_abc123",
"campaignId": "cmp_xyz789",
"creativeId": "crt_def456",
"eventType": "purchase",
"value": 99.99,
"currency": "USD",
"orderId": "order_12345",
"metadata": {
"productName": "VIP Event Ticket",
"quantity": 2
}
}
}{
"specversion": "1.0",
"id": "evt_billing_low_001",
"source": "eventzr.ads-svc",
"type": "eventzr.ads.billing.low_balance.v1",
"time": "2026-03-09T08:00:00.000Z",
"subject": "tenant_abc123",
"tenantid": "tenant_abc123",
"data": {
"currentBalance": 25.50,
"currency": "USD",
"threshold": 100.00,
"activeCampaigns": 3,
"estimatedDaysRemaining": 1.2
}
}Register a webhook endpoint to receive events via HTTP POST callbacks. You can subscribe to specific event types or receive all events.
// Register a webhook endpoint via the SDK
const webhook = await client.webhooks.create({
url: 'https://yourapp.com/webhooks/ads',
events: [
'eventzr.ads.campaign.launched.v1',
'eventzr.ads.conversion.tracked.v1',
'eventzr.ads.billing.low_balance.v1',
],
secret: 'whsec_your_signing_secret_here',
active: true,
});
console.log('Webhook ID:', webhook.data.id);
// List registered webhooks
const webhooks = await client.webhooks.list();
// Update webhook
await client.webhooks.update(webhook.data.id, {
events: ['*'], // Subscribe to all events
});
// Delete webhook
await client.webhooks.delete(webhook.data.id);Every webhook delivery includes an x-eventzr-signature header containing an HMAC-SHA256 signature of the request body. Always verify this signature to ensure the payload is authentic and has not been tampered with.
import { createHmac, timingSafeEqual } from 'crypto';
import type { Request, Response } from 'express';
const WEBHOOK_SECRET = process.env.ADS_WEBHOOK_SECRET!;
function verifySignature(payload: string, signature: string): boolean {
const expected = createHmac('sha256', WEBHOOK_SECRET)
.update(payload, 'utf8')
.digest('hex');
const sig = Buffer.from(signature, 'hex');
const exp = Buffer.from(expected, 'hex');
if (sig.length !== exp.length) return false;
return timingSafeEqual(sig, exp);
}
// Express webhook handler
app.post('/webhooks/ads', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
const signature = req.headers['x-eventzr-signature'] as string;
const timestamp = req.headers['x-eventzr-timestamp'] as string;
// 1. Reject if timestamp is older than 5 minutes (replay protection)
const eventTime = parseInt(timestamp, 10);
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - eventTime) > 300) {
return res.status(400).json({ error: 'Timestamp too old' });
}
// 2. Verify HMAC signature
const payload = req.body.toString('utf8');
if (!verifySignature(payload, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 3. Process the event
const event = JSON.parse(payload);
handleAdsEvent(event);
// 4. Respond with 200 to acknowledge receipt
return res.status(200).json({ received: true });
});If your endpoint does not respond with a 2xx status within 10 seconds, the delivery is retried with exponential backoff.
| Attempt | Delay After Failure | Cumulative Time |
|---|---|---|
| 1 (initial) | Immediate | 0s |
| 2 | 30 seconds | 30s |
| 3 | 2 minutes | 2m 30s |
| 4 | 10 minutes | 12m 30s |
| 5 | 1 hour | 1h 12m 30s |
| 6 (final) | 4 hours | 5h 12m 30s |
After 6 failed attempts, the event is sent to a dead-letter queue. You can replay failed events from the webhook dashboard or via the API.
Return a 200 response immediately and process the event asynchronously. If processing takes longer than 10 seconds, the delivery will be marked as failed and retried.
Due to retries, your endpoint may receive the same event more than once. Use the event "id" field as an idempotency key to deduplicate.
Always verify the HMAC-SHA256 signature before processing. Never skip verification, even in development or staging environments.
Only subscribe to events you need. High-volume events like impression.tracked can generate significant traffic. Use Kafka consumers for high-throughput event processing.
For high-throughput use cases (e.g., processing millions of impression events), you can consume events directly from the Kafka topics instead of using HTTP webhooks.
import { KafkaBrokerModule } from '@eventzr/kafka';
@Module({
imports: [
KafkaBrokerModule.forConsumer({
groupId: 'your-app-ads-consumer',
topics: [
'eventzr.ads.impression.tracked.v1',
'eventzr.ads.conversion.tracked.v1',
],
}),
],
})
export class AdsEventsModule {}
// Consumer handler
@Injectable()
export class AdsEventConsumer {
@KafkaHandler('eventzr.ads.impression.tracked.v1')
async handleImpression(event: CloudEvent<ImpressionData>): Promise<void> {
// Process impression event at scale
await this.analyticsService.recordImpression(event.data);
}
@KafkaHandler('eventzr.ads.conversion.tracked.v1')
async handleConversion(event: CloudEvent<ConversionData>): Promise<void> {
// Process conversion for attribution
await this.attributionService.attribute(event.data);
}
}