Campaign Reporting
Pull delivery data for campaigns registered with use case campaign_optimization or campaign_reporting. See exactly where your ads served, with optional breakdowns by date, domain, device, and geography.
Reporting is asynchronous. Submit a report request, receive an ID, and poll for results.
Set include_classification: true to have Classify analyze the URLs where your ads served. The report will include IAB categories, keywords, entities, and sentiment for each URL — even if you didn't use Classify segments to target.
The campaign report object
{
"id": 402,
"status": "complete",
"campaign_id": 609,
"start_date": "2026-01-15",
"end_date": "2026-02-14",
"dimensions": ["date", "domain"],
"include_classification": false,
"created_date": "2026-02-16T10:00:00Z",
"processed_date": "2026-02-16T10:03:12Z",
"summary": {
"impressions": 48231,
"unique_urls": 1847,
"unique_domains": 312
},
"data": [...]
}
| Field | Type | Description |
|---|---|---|
id | integer | Unique report ID |
status | string | pending → processing → complete or failed |
campaign_id | integer | The pixel campaign this report is for |
start_date | string (ISO 8601) | Report start date (inclusive) |
end_date | string (ISO 8601) | Report end date (inclusive) |
dimensions | array[string] | null | Breakdown dimensions requested |
include_classification | boolean | Whether contextual classification data is included |
created_date | string (ISO 8601) | When the report was requested |
processed_date | string (ISO 8601) | null | When results were ready. null until complete. |
summary | object | null | Aggregate totals. Present when status is complete. |
data | array[object] | null | Row-level data. Present when status is complete. |
Create a campaign report
POST https://api.clsfy.me/v1/pixel/reports/campaign
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_id | integer | Required | The pixel campaign ID |
start_date | string (ISO 8601) | Required | Start of the reporting period |
end_date | string (ISO 8601) | Required | End of the reporting period (inclusive) |
dimensions | array[string] | Optional | Breakdown dimensions. Values: date, domain, url, device, geo. Omit for summary only. |
include_classification | boolean | Optional | Include contextual classification data (IAB categories, keywords, entities, sentiment) for served URLs. Default false. |
filters | object | Optional | Narrow results. Supported keys: domains, devices, geo. |
Filter values:
domains: array of domain strings (e.g.["nytimes.com", "bbc.com"])devices:desktop,mobile,tablet,ctvgeo: ISO 3166-1 alpha-2 country codes
Request
- curl
- Python
curl -X POST "https://api.clsfy.me/v1/pixel/reports/campaign" \
-H "X-API-Key: <your_api_key>" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": 609,
"start_date": "2026-01-15",
"end_date": "2026-02-14",
"dimensions": ["date", "domain"],
"include_classification": true
}'
import requests
response = requests.post(
"https://api.clsfy.me/v1/pixel/reports/campaign",
headers={
"X-API-Key": "<your_api_key>",
"Content-Type": "application/json",
},
json={
"campaign_id": 609,
"start_date": "2026-01-15",
"end_date": "2026-02-14",
"dimensions": ["date", "domain"],
"include_classification": True,
},
)
report = response.json()
print(report["id"]) # e.g. 402
Response
{
"id": 402,
"status": "pending",
"campaign_id": 609,
"start_date": "2026-01-15",
"end_date": "2026-02-14",
"dimensions": ["date", "domain"],
"include_classification": true,
"created_date": "2026-02-16T10:00:00Z",
"processed_date": null,
"summary": null,
"data": null
}
Get a campaign report
GET https://api.clsfy.me/v1/pixel/reports/campaign/{id}
| Parameter | Type | Description |
|---|---|---|
id | integer | The report ID |
limit | integer (query) | Results per page. Default 1000, max 10000. |
offset | integer (query) | Results to skip. Default 0. |
- curl
- Python
curl "https://api.clsfy.me/v1/pixel/reports/campaign/402" \
-H "X-API-Key: <your_api_key>"
import requests
response = requests.get(
"https://api.clsfy.me/v1/pixel/reports/campaign/402",
headers={"X-API-Key": "<your_api_key>"},
)
report = response.json()
if report["status"] == "complete":
print(f"Impressions: {report['summary']['impressions']}")
print(f"Domains: {report['summary']['unique_domains']}")
Completed response
{
"id": 402,
"status": "complete",
"campaign_id": 609,
"start_date": "2026-01-15",
"end_date": "2026-02-14",
"dimensions": ["date", "domain"],
"include_classification": true,
"created_date": "2026-02-16T10:00:00Z",
"processed_date": "2026-02-16T10:03:12Z",
"summary": {
"impressions": 48231,
"unique_urls": 1847,
"unique_domains": 312
},
"data": [
{
"date": "2026-01-15",
"domain": "nytimes.com",
"impressions": 142,
"classification": {
"iab_categories": [
{"id": "IAB19-6", "name": "Technology & Computing", "confidence": 0.93}
],
"keywords": ["AI chips", "semiconductor", "GPU"],
"entities": [
{"name": "NVIDIA", "type": "brand", "confidence": 0.91}
],
"sentiment": {"label": "positive", "score": 0.72}
}
},
{
"date": "2026-01-15",
"domain": "bbc.com",
"impressions": 89,
"classification": {
"iab_categories": [
{"id": "IAB17-44", "name": "Sports", "confidence": 0.96}
],
"keywords": ["football", "Premier League"],
"entities": [
{"name": "Premier League", "type": "thing", "confidence": 0.94}
],
"sentiment": {"label": "neutral", "score": 0.51}
}
}
]
}
When include_classification is false, the classification field is omitted from each data row.
Macro data
If your pixel code includes macros, their expanded values appear as additional fields in each data row. For example, if your pixel passes google-site=%%SITE%%:
{
"date": "2026-01-15",
"domain": "nytimes.com",
"impressions": 142,
"macros": {
"google-site": "nytimes.com"
}
}
Compare the macros.google-site value against the domain to detect inventory misrepresentation.
Polling for results
- Python
import requests
import time
def wait_for_campaign_report(report_id: int, api_key: str, poll_interval: int = 30):
"""Poll until a campaign report is ready."""
url = f"https://api.clsfy.me/v1/pixel/reports/campaign/{report_id}"
headers = {"X-API-Key": api_key}
while True:
report = requests.get(url, headers=headers).json()
if report["status"] == "complete":
print(f"Done — {report['summary']['impressions']} impressions")
return report
elif report["status"] == "failed":
raise RuntimeError(f"Report {report_id} failed.")
print(f"Status: {report['status']} — retrying in {poll_interval}s")
time.sleep(poll_interval)
Dimensions reference
| Dimension | Value | Granularity |
|---|---|---|
| Date | date | Daily |
| Domain | domain | Publisher domain |
| URL | url | Individual page URL |
| Device | device | desktop, mobile, tablet, ctv |
| Geography | geo | Country (ISO 3166-1 alpha-2) |
Error responses
When a request fails, the API returns a JSON object with an error code and a human-readable message:
{
"error": "not_found",
"message": "Campaign report with ID 999 not found"
}
HTTP status codes
| Status | Meaning |
|---|---|
200 OK | Success |
201 Created | Resource created (POST endpoints) |
400 Bad Request | Invalid or missing parameters |
401 Unauthorized | Missing or invalid API key |
404 Not Found | Resource not found |
422 Unprocessable Content | Validation error (e.g. invalid field values) |
429 Too Many Requests | Rate limit exceeded |