Campaigns
Campaign performance, ROAS, ad-level breakdowns, and daily spend trends.
Top Campaigns
GET /api/v1/campaignsReturns top-performing campaigns ranked by attributed outcome value, with ROAS
and spend data. Pass level to drill down to ad sets or individual ads.
Scope: campaigns:read
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date_from | string | Yes | Start date in YYYY-MM-DD format. |
date_to | string | Yes | End date in YYYY-MM-DD format. |
model | string | No | Attribution model. Default last_touch. Options: first_touch, linear, time_decay, last_non_direct. |
limit | number | No | Maximum results. Default 10, max 100. |
level | string | No | Hierarchy level: campaign, ad_set, or ad. When set, returns detailed performance fields. |
Request
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://www.atribu.app/api/v1/campaigns?date_from=2026-03-01&date_to=2026-03-25&model=last_touch&limit=5"const res = await fetch(
"https://www.atribu.app/api/v1/campaigns?date_from=2026-03-01&date_to=2026-03-25&model=last_touch&limit=5",
{ headers: { Authorization: "Bearer atb_live_YOUR_KEY" } }
);
const { data, meta } = await res.json();import requests
res = requests.get(
"https://www.atribu.app/api/v1/campaigns",
headers={"Authorization": "Bearer atb_live_YOUR_KEY"},
params={
"date_from": "2026-03-01",
"date_to": "2026-03-25",
"model": "last_touch",
"limit": 5,
},
)
data = res.json()["data"]Response
{
"data": [
{
"campaign_id": "uuid",
"campaign_name": "Spring Sale - Conversions",
"spend": 1800.00,
"outcome_count": 45,
"outcome_value": 6200.00,
"roas": 3.44,
"attributed_outcome_count": 189,
"attributed_outcomes_by_type": {
"appointment_attended": 2,
"appointment_booked": 4,
"closed_won": 1,
"dm_started": 89,
"lead_created": 88,
"payment_received": 2,
"quote_presented": 3
}
}
],
"meta": {
"date_from": "2026-03-01",
"date_to": "2026-03-25",
"profile_id": "uuid"
}
}Response fields
| Field | Type | Description |
|---|---|---|
campaign_id | string | Internal campaign UUID. |
campaign_name | string | Human-readable campaign name from the ad platform. |
spend | number | Total ad spend in your reporting currency. |
outcome_count | number | Cash conversions (payments) credited to this campaign under model. The ROAS/CAC denominator. |
outcome_value | number | Total attributed cash revenue. |
roas | number | Return on ad spend (outcome_value / spend). |
attributed_outcome_count | number | Conversions of every kind credited to this campaign under model — leads, DMs started, appointments, quotes, closed deals and cash payments (refunds excluded). Not cash: never use it for ROAS or CAC. |
attributed_outcomes_by_type | object | attributed_outcome_count split by conversion type ({"dm_started": 89, "lead_created": 88, …}). Values sum to attributed_outcome_count; a type with nothing credited is absent. |
Lead-gen and messaging accounts
outcome_count counts cash only, so a clinic that books and closes through
its CRM or WhatsApp and never takes payment through Atribu sees 0 there.
attributed_outcome_count and its attributed_outcomes_by_type split are
the funnel for those accounts.
Every count is conversions credited to this entity under the model: a
conversion counts for a campaign only if that campaign received credit, not
merely because a touch on it appeared somewhere in the path. Under a
single-credit model (last_touch, first_touch, last_non_direct) the rows
plus GET /api/v1/campaigns/unattributed add up to the profile's
total; under linear or time_decay a conversion credited to two campaigns
counts under both. Compare counts only within one model and never add one
model's figures to another's.
Ad-level detail
Pass level=ad to get granular performance per ad creative. This adds
additional fields to each row.
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://www.atribu.app/api/v1/campaigns?date_from=2026-03-01&date_to=2026-03-25&level=ad"const res = await fetch(
"https://www.atribu.app/api/v1/campaigns?date_from=2026-03-01&date_to=2026-03-25&level=ad",
{ headers: { Authorization: "Bearer atb_live_YOUR_KEY" } }
);
const { data } = await res.json();res = requests.get(
"https://www.atribu.app/api/v1/campaigns",
headers={"Authorization": "Bearer atb_live_YOUR_KEY"},
params={"date_from": "2026-03-01", "date_to": "2026-03-25", "level": "ad"},
)
data = res.json()["data"]Additional fields when level is set
| Field | Type | Description |
|---|---|---|
entity_level | string | The level of this row (campaign, ad_set, ad). |
parent_name | string | Name of the parent entity (campaign or ad set). |
impressions | number | Total impressions served. |
clicks | number | Total clicks. |
reach | number | Unique accounts reached. |
ctr | number | Click-through rate (%). |
avg_cpm | number | Average cost per 1,000 impressions. |
avg_cpc | number | Average cost per click. |
cac | number | Customer acquisition cost (spend / outcome_count). |
status | string | Entity status (ACTIVE, PAUSED, etc.). |
objective | string | Campaign objective. |
daily_budget | number | Daily budget if set on the platform. |
campaign_external_id / ad_set_external_id | string | null | The parent campaign / ad set ids, for drilling campaign → ad set → ad. |
messaging_conversations_started_7d | number | Conversations started (7-day window) as reported by the platform. |
attributed_outcome_count / attributed_outcomes_by_type | number / object | Conversions of every kind credited to this entity under model (not cash), and their split by type — see above. Also on GET /api/v1/ads/{id} and on the GET /api/v1/campaigns/unattributed remainder. |
Campaign Trend
GET /api/v1/campaigns/trendReturns daily spend, impressions, and clicks for up to 10 specific campaigns, ad sets, or ads. Use this to render sparklines or compare entity performance over time.
Scope: campaigns:read
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date_from | string | Yes | Start date in YYYY-MM-DD format. |
date_to | string | Yes | End date in YYYY-MM-DD format. |
entity_ids | string | Yes | Comma-separated list of entity UUIDs (max 10). |
level | string | No | campaign, ad_set, or ad. Default campaign. |
Request
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://www.atribu.app/api/v1/campaigns/trend?date_from=2026-03-20&date_to=2026-03-25&entity_ids=uuid1,uuid2&level=campaign"const ids = ["uuid1", "uuid2"].join(",");
const res = await fetch(
`https://www.atribu.app/api/v1/campaigns/trend?date_from=2026-03-20&date_to=2026-03-25&entity_ids=${ids}&level=campaign`,
{ headers: { Authorization: "Bearer atb_live_YOUR_KEY" } }
);
const { data } = await res.json();import requests
res = requests.get(
"https://www.atribu.app/api/v1/campaigns/trend",
headers={"Authorization": "Bearer atb_live_YOUR_KEY"},
params={
"date_from": "2026-03-20",
"date_to": "2026-03-25",
"entity_ids": "uuid1,uuid2",
"level": "campaign",
},
)
data = res.json()["data"]Response
{
"data": [
{ "entity_id": "uuid1", "date": "2026-03-20", "spend": 120.00, "impressions": 15000, "clicks": 340 },
{ "entity_id": "uuid1", "date": "2026-03-21", "spend": 135.00, "impressions": 16200, "clicks": 380 },
{ "entity_id": "uuid2", "date": "2026-03-20", "spend": 95.00, "impressions": 11800, "clicks": 275 }
],
"meta": {
"date_from": "2026-03-20",
"date_to": "2026-03-25",
"profile_id": "uuid"
}
}Response fields
| Field | Type | Description |
|---|---|---|
entity_id | string | The campaign, ad set, or ad UUID. |
date | string | Calendar date (YYYY-MM-DD). |
spend | number | Ad spend for that entity on that day. |
impressions | number | Impressions served. |
clicks | number | Clicks recorded. |
Entity ID format
entity_ids expects Atribu internal UUIDs (from the campaign_id field in
the Top Campaigns response), not platform-specific IDs. You can pass up to
10 IDs in a single request.
Top Performers
GET /api/v1/top-performersThe profile's best-performing ads, scored against comparable creative — a cohort = channel × format × objective × audience warmth × geo × placement. The score is cohort-normalized, empirical-Bayes-smoothed (a small-sample ad shrinks toward the cohort mean — it lands mid-pack, the honest "we don't know yet"), and maturity-staged. So a $500/mo clinic and a $30k/mo clinic are ranked apples-to-apples.
Each ad carries three distinct measures — never merge them:
composite_score(0–100) — a transparent rule-based blend of the ad's within-cohort percentiles across funnel layers. The headline rank.top_performer_likelihood— a learned field{ value, status, reason }: the LambdaMART ranker's 0–1 probability (a likelihood, not a guarantee),servedonly when a ranker has shown out-of-time skill above chance and scored this ad;withheldotherwise (show «not evaluated», never 0) andnot_applicableon arulesrow. Readvalueonly whenstatusisserved. It is not ROAS and not lift. The Cox fatigue values (hazard_30d_*,fatigue_risk_tier,expected_lifespan_days) have the same shape.attributed_revenue/roas(andattributed_outcomes_total/attributed_pipeline_outcomes/attributed_pipeline_value) — real Atribu-attributed outcomes.truth_grade='attributed'means Atribu deterministically attributed ≥1 outcome of any kind to this ad (cash payments or pipeline conversions — leads, appointments, closed-won).meta_reported_conversions/meta_reported_conversion_valueare a separate, lower-confidence signal — Meta's own last-click count for the ad, not Atribu's chain; they never settruth_grade.
primary_outcome_kind ∈ cash · pipeline · messaging · meta · none tells you which of
the above to headline for a given ad (a lead-gen ad shows pipeline outcomes, a Messages ad shows
conversations started, an ad with only Meta-reported conversions shows meta_reported_conversions
labeled as Meta's attribution, none = no attributed outcome yet → use composite_score).
maturity_stage (cold → early → mature → calibrated) says how settled
the score is — act on mature/calibrated, treat cold as provisional.
reason_codes explain why an ad ranks (which funnel layer is strong or weak,
each with a sample_confidence band). Scores refresh on a daily cadence.
Scope: campaigns:read
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
window | string | No | Rolling lookback: 7d, 14d, 28d (default), or lifetime. Not a custom date range. |
limit | number | No | Maximum ads. Default 10, max 50. |
min_spend | number | No | Only ads with at least this much spend in the window. |
objectives | string | No | Comma-separated cohort objectives to keep (messaging, sales, leads, traffic, awareness). |
truth_grades | string | No | Comma-separated: predicted, attributed, lift. |
outcome_kinds | string | No | Comma-separated headline-outcome kinds to keep: cash, pipeline, messaging, meta, none. |
formats | string | No | Comma-separated creative formats (ugc_video, talking_head, static, carousel, …). |
has_video | boolean | No | true to keep only ads with a video creative; false for only static. |
Request
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://www.atribu.app/api/v1/top-performers?window=28d&limit=5&truth_grades=attributed"Response
{
"data": [
{
"ad_external_id": "120210000000000123",
"ad_name": "UGC — before/after — DM offer",
"campaign_name": "Always-on — Messaging",
"ad_set_name": "Prospecting — broad",
"creative_thumbnail_url": "https://…",
"video_id": "987654321",
"score_window": "28d",
"composite_score": 78,
"top_performer_likelihood": { "status": "withheld", "value": null, "reason": "unmeasured" },
"model_version": 3,
"score_source": "model",
"maturity_stage": "mature",
"truth_grade": "attributed",
"primary_outcome_kind": "messaging",
"cohort_key": "meta|ugc_video|messaging|prospecting|geo_any|advantage",
"cohort_objective": "messaging",
"cohort_format": "ugc_video",
"cohort_audience_warmth": "prospecting",
"cohort_n_ads": 17,
"hook_type": "before_after",
"creative_format": "ugc_video",
"cta_type": "send_dm",
"primary_angle": "fast visible results",
"offers": ["free consultation"],
"spend": 4120.0,
"impressions": 512000,
"ctr": 1.42,
"cpc": 0.57,
"attributed_cash_outcomes": 6,
"attributed_revenue": 9200.0,
"roas": 2.23,
"cac": 686.67,
"attributed_outcomes_total": 18,
"attributed_pipeline_outcomes": 12,
"attributed_pipeline_value": 6400.0,
"meta_reported_conversions": 0,
"meta_reported_conversion_value": 0.0,
"cost_per_conversation": 0.33,
"messaging_conversations_started": 412,
"first_reply_rate": 41.2,
"avg_engagement_score": null,
"reason_codes": [
{ "layer": "attention", "polarity": "strength", "percentile": 88, "sample_confidence": "high" },
{ "layer": "postclick_messaging", "polarity": "strength", "percentile": 91, "sample_confidence": "high" },
{ "layer": "attributed_revenue", "polarity": "strength", "percentile": 74, "sample_confidence": "medium" }
]
}
],
"meta": {
"profile_id": "uuid"
}
}Response fields
| Field | Type | Description |
|---|---|---|
ad_external_id | string | Platform ad id (e.g. Meta ad id). |
composite_score | number | 0–100, the cohort-normalized rule blend. null until the ad has been scored. The headline rank. |
top_performer_likelihood | object | { value, status, reason } — a learned 0–1 probability, served only past the serving gate. Not ROAS, not lift — see the note above. |
model_version | number | The ML ranker version that scored this ad, or null when score_source is rules. |
score_source | string | model (the ML ranker produced the likelihood) or rules (it fell back to composite_score). |
maturity_stage | string | cold · early · mature · calibrated — how settled the score is. |
truth_grade | string | predicted (proxy signals only) · attributed (≥1 Atribu-attributed outcome of any kind) · lift (experiment-validated). |
primary_outcome_kind | string | The headline outcome to show: cash · pipeline · messaging · meta (Meta's own attribution) · none. |
cohort_* | string | The comparison cohort this ad was scored within, plus cohort_n_ads (cohort size). |
hook_type / creative_format / cta_type / primary_angle / offers | mixed | Creative DNA — what to replicate. |
attributed_revenue / roas / attributed_cash_outcomes / cac | number | Atribu-attributed cash (null/0 when none). |
attributed_outcomes_total / attributed_pipeline_outcomes / attributed_pipeline_value | number | All Atribu-attributed conversions (any type); the pipeline subset (leads/appointments/closed-won) + its projected value (not cash). |
meta_reported_conversions / meta_reported_conversion_value | number | Meta's own reported per-ad conversions (last-click — not Atribu's chain; never sets truth_grade). |
cost_per_conversation / messaging_conversations_started / first_reply_rate | number | Messaging-objective signals (the "cheap conversations" lens). |
reason_codes | array | { layer, polarity: strength|weakness, percentile, sample_confidence: low|medium|high } — why it ranks. |
Workspace-wide view
This endpoint is scoped to the API key's profile. The agency-wide
cross-profile Top Performers (all clients in one ranked list, plus recurring
creative patterns) is available through the Atribu MCP server —
top_workspace_performers. See the MCP docs.
Scores build on a daily cadence
Top-performer scores rebuild once a day (independent of the latency-sensitive
attribution recompute). A freshly connected Meta account needs one sync cycle
before its ads appear here. An empty data array usually means "not scored
yet", not "no top performers".
Platform vs Atribu
GET /api/v1/campaigns/platform-comparisonWhat each ad platform reports it produced, next to what Atribu attributed to the same campaign, ad set or ad under one model. The two are never added together: platforms credit view-through and modeled conversions, use their own windows and date a conversion to the ad interaction; Atribu counts only conversions it can trace to a click, dates them when they happened, and values them by cash collected.
Scope: campaigns:read
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date_from | string | Yes | Start date, YYYY-MM-DD. Up to 366 days. |
date_to | string | Yes | End date, YYYY-MM-DD, inclusive. |
model | string | No | Attribution model for the Atribu side. Default last_touch. |
level | string | No | campaign, ad_set or ad. Default campaign. |
Request
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://api.atribu.app/api/v1/campaigns/platform-comparison?date_from=2026-08-25&date_to=2026-09-23&level=campaign"Response fields
data.platforms[] holds one total per platform and data.rows[] one row per
entity at level. Both carry the same measures:
| Field | Type | Description |
|---|---|---|
platform_conversions | number | null | The platform's own count. null when it reported nothing. |
platform_conversion_value | string | null | The platform's own value, in the reporting currency. |
atribu_conversions / atribu_cash_conversions | integer | Conversions Atribu credited here, and the cash subset. |
atribu_cash_revenue | string | Cash Atribu credited here (refunds subtracted). |
conversion_gap / conversion_gap_pct | number | null | Atribu minus platform. Negative: the platform claims more. |
value_gap | string | null | Atribu cash minus platform value, exact. |
platform_roas / atribu_roas | number | null | Each side's value over spend. |
Each platform total also says what its count means (reported_basis), whether
delivery was synced (has_delivery, delivery_through), and how much of
Atribu's credit came from paid clicks that named no ad (unassigned_conversions,
unassigned_cash_revenue — see GET /api/v1/quality/ad-ids).
People reached by ads
GET /api/v1/attribution/ad-peopleHow many distinct people had at least one ad-tagged Touch in each month, day or ad of the window, where those Touches came from, and what Meta itself reported for the same bucket (messaging conversations started), so the gap between the two is visible.
The response is counts only: no person is named, listed or identified, and
no figure is a probability or a model's estimate. It is available for
healthcare businesses under analytics:read.
Scope: analytics:read
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date_from | string | Yes | Start date, YYYY-MM-DD, in the profile's time zone. Up to 366 days. |
date_to | string | Yes | End date, YYYY-MM-DD, inclusive. |
group_by | string | No | month (default), day or ad. |
channel | string | No | whatsapp, instagram, messenger, web or other. |
first_only | string | No | true counts each person only in the bucket of their earliest-ever ad Touch. Default false. |
limit | string | No | group_by=ad only: ads to return, 1 to 1000. Default 100. |
Request
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://api.atribu.app/api/v1/attribution/ad-people?date_from=2026-02-01&date_to=2026-09-30&group_by=month"Response
{
"data": {
"date_from": "2026-02-01",
"date_to": "2026-09-30",
"group_by": "month",
"channel": null,
"first_only": false,
"timezone": "America/Santiago",
"totals": {
"ad_people": 3839,
"ad_touches": 4465,
"by_origin": {
"partner_ad_referral": { "people": 3584, "touches": 4165 },
"conversation_referral": { "people": 300, "touches": 300 },
"web": { "people": 0, "touches": 0 },
"other": { "people": 0, "touches": 0 }
},
"meta_messaging_conversations_started": 9704
},
"buckets": [
{
"key": "2026-09",
"start": "2026-09-01",
"end": "2026-09-30",
"ad_id": null,
"ad_name": null,
"ad_people": 1050,
"ad_touches": 1073,
"by_origin": {
"partner_ad_referral": { "people": 763, "touches": 773 },
"conversation_referral": { "people": 300, "touches": 300 },
"web": { "people": 0, "touches": 0 },
"other": { "people": 0, "touches": 0 }
},
"meta_messaging_conversations_started": 1326
}
],
"buckets_total": 8,
"buckets_truncated": false,
"origin_labels": {
"partner_ad_referral": {
"label_en": "Imported from partner (historical)",
"label_es": "Importado desde partner (historial)",
"description": "..."
}
}
},
"meta": { "profile_id": "uuid", "date_from": "2026-02-01", "date_to": "2026-09-30" }
}Response fields
| Field | Type | Description |
|---|---|---|
ad_people | integer | Distinct people with at least one ad Touch in the bucket. |
ad_touches | integer | Ad Touches in the bucket. Equal to ad_people with first_only=true. |
by_origin.partner_ad_referral | object | Imported from a partner (historical): ad referrals a partner handed over through POST /api/v1/partner/ad-referrals for conversations Atribu did not see live. |
by_origin.conversation_referral | object | A WhatsApp or Instagram conversation Atribu received with Meta's ad referral. |
by_origin.web | object | A tracked website visit whose link named the ad. |
by_origin.other | object | Any other ad-tagged Touch (a CRM conversion's own ad parameters, a store journey, a Touch inherited by a recurring payment). |
meta_messaging_conversations_started | integer | null | Meta's own count for the bucket, by the ad account's day. Not narrowed by channel. null when there is no Meta delivery to report. |
totals | object | The whole window with each person counted once. |
origin_labels | object | English and Spanish labels for each by_origin key. |
Days and months are in the profile's time zone (timezone). A person reached
in two months counts once in each month and once in totals, so totals is
not the sum of the buckets, and a person reached through two origins counts in
both origins. Month and day buckets are dense (zeros included); ad buckets are
ranked by ad_people and cut at limit (buckets_truncated). The ad is read
from the Touch itself, never inferred from UTM text. Durable tables only, so any
range up to 366 days is supported.
GET /api/v1/ads/trend also carries Meta's messaging_conversations_started_7d
per ad and day.