Atribu
API Reference

Campaigns

Campaign performance, ROAS, ad-level breakdowns, and daily spend trends.

Top Campaigns

Endpoint
GET /api/v1/campaigns

Returns 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

ParameterTypeRequiredDescription
date_fromstringYesStart date in YYYY-MM-DD format.
date_tostringYesEnd date in YYYY-MM-DD format.
modelstringNoAttribution model. Default last_touch. Options: first_touch, linear, time_decay, last_non_direct.
limitnumberNoMaximum results. Default 10, max 100.
levelstringNoHierarchy level: campaign, ad_set, or ad. When set, returns detailed performance fields.

Request

cURL
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"
JavaScript
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();
Python
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

Success response (200 OK)
{
  "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

FieldTypeDescription
campaign_idstringInternal campaign UUID.
campaign_namestringHuman-readable campaign name from the ad platform.
spendnumberTotal ad spend in your reporting currency.
outcome_countnumberCash conversions (payments) credited to this campaign under model. The ROAS/CAC denominator.
outcome_valuenumberTotal attributed cash revenue.
roasnumberReturn on ad spend (outcome_value / spend).
attributed_outcome_countnumberConversions 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_typeobjectattributed_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 — ad-level breakdown
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"
JavaScript — ad-level breakdown
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();
Python — ad-level breakdown
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

FieldTypeDescription
entity_levelstringThe level of this row (campaign, ad_set, ad).
parent_namestringName of the parent entity (campaign or ad set).
impressionsnumberTotal impressions served.
clicksnumberTotal clicks.
reachnumberUnique accounts reached.
ctrnumberClick-through rate (%).
avg_cpmnumberAverage cost per 1,000 impressions.
avg_cpcnumberAverage cost per click.
cacnumberCustomer acquisition cost (spend / outcome_count).
statusstringEntity status (ACTIVE, PAUSED, etc.).
objectivestringCampaign objective.
daily_budgetnumberDaily budget if set on the platform.
campaign_external_id / ad_set_external_idstring | nullThe parent campaign / ad set ids, for drilling campaign → ad set → ad.
messaging_conversations_started_7dnumberConversations started (7-day window) as reported by the platform.
attributed_outcome_count / attributed_outcomes_by_typenumber / objectConversions 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

Endpoint
GET /api/v1/campaigns/trend

Returns 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

ParameterTypeRequiredDescription
date_fromstringYesStart date in YYYY-MM-DD format.
date_tostringYesEnd date in YYYY-MM-DD format.
entity_idsstringYesComma-separated list of entity UUIDs (max 10).
levelstringNocampaign, ad_set, or ad. Default campaign.

Request

cURL
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"
JavaScript
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();
Python
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

Success response (200 OK)
{
  "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

FieldTypeDescription
entity_idstringThe campaign, ad set, or ad UUID.
datestringCalendar date (YYYY-MM-DD).
spendnumberAd spend for that entity on that day.
impressionsnumberImpressions served.
clicksnumberClicks 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

Endpoint
GET /api/v1/top-performers

The 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), served only when a ranker has shown out-of-time skill above chance and scored this ad; withheld otherwise (show «not evaluated», never 0) and not_applicable on a rules row. Read value only when status is served. 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 (and attributed_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_value are a separate, lower-confidence signal — Meta's own last-click count for the ad, not Atribu's chain; they never set truth_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

ParameterTypeRequiredDescription
windowstringNoRolling lookback: 7d, 14d, 28d (default), or lifetime. Not a custom date range.
limitnumberNoMaximum ads. Default 10, max 50.
min_spendnumberNoOnly ads with at least this much spend in the window.
objectivesstringNoComma-separated cohort objectives to keep (messaging, sales, leads, traffic, awareness).
truth_gradesstringNoComma-separated: predicted, attributed, lift.
outcome_kindsstringNoComma-separated headline-outcome kinds to keep: cash, pipeline, messaging, meta, none.
formatsstringNoComma-separated creative formats (ugc_video, talking_head, static, carousel, …).
has_videobooleanNotrue to keep only ads with a video creative; false for only static.

Request

cURL
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
  "https://www.atribu.app/api/v1/top-performers?window=28d&limit=5&truth_grades=attributed"

Response

Success response (200 OK)
{
  "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

FieldTypeDescription
ad_external_idstringPlatform ad id (e.g. Meta ad id).
composite_scorenumber0–100, the cohort-normalized rule blend. null until the ad has been scored. The headline rank.
top_performer_likelihoodobject{ value, status, reason } — a learned 0–1 probability, served only past the serving gate. Not ROAS, not lift — see the note above.
model_versionnumberThe ML ranker version that scored this ad, or null when score_source is rules.
score_sourcestringmodel (the ML ranker produced the likelihood) or rules (it fell back to composite_score).
maturity_stagestringcold · early · mature · calibrated — how settled the score is.
truth_gradestringpredicted (proxy signals only) · attributed (≥1 Atribu-attributed outcome of any kind) · lift (experiment-validated).
primary_outcome_kindstringThe headline outcome to show: cash · pipeline · messaging · meta (Meta's own attribution) · none.
cohort_*stringThe comparison cohort this ad was scored within, plus cohort_n_ads (cohort size).
hook_type / creative_format / cta_type / primary_angle / offersmixedCreative DNA — what to replicate.
attributed_revenue / roas / attributed_cash_outcomes / cacnumberAtribu-attributed cash (null/0 when none).
attributed_outcomes_total / attributed_pipeline_outcomes / attributed_pipeline_valuenumberAll Atribu-attributed conversions (any type); the pipeline subset (leads/appointments/closed-won) + its projected value (not cash).
meta_reported_conversions / meta_reported_conversion_valuenumberMeta's own reported per-ad conversions (last-click — not Atribu's chain; never sets truth_grade).
cost_per_conversation / messaging_conversations_started / first_reply_ratenumberMessaging-objective signals (the "cheap conversations" lens).
reason_codesarray{ 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

Endpoint
GET /api/v1/campaigns/platform-comparison

What 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

ParameterTypeRequiredDescription
date_fromstringYesStart date, YYYY-MM-DD. Up to 366 days.
date_tostringYesEnd date, YYYY-MM-DD, inclusive.
modelstringNoAttribution model for the Atribu side. Default last_touch.
levelstringNocampaign, ad_set or ad. Default campaign.

Request

cURL
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:

FieldTypeDescription
platform_conversionsnumber | nullThe platform's own count. null when it reported nothing.
platform_conversion_valuestring | nullThe platform's own value, in the reporting currency.
atribu_conversions / atribu_cash_conversionsintegerConversions Atribu credited here, and the cash subset.
atribu_cash_revenuestringCash Atribu credited here (refunds subtracted).
conversion_gap / conversion_gap_pctnumber | nullAtribu minus platform. Negative: the platform claims more.
value_gapstring | nullAtribu cash minus platform value, exact.
platform_roas / atribu_roasnumber | nullEach 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

Endpoint
GET /api/v1/attribution/ad-people

How 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

ParameterTypeRequiredDescription
date_fromstringYesStart date, YYYY-MM-DD, in the profile's time zone. Up to 366 days.
date_tostringYesEnd date, YYYY-MM-DD, inclusive.
group_bystringNomonth (default), day or ad.
channelstringNowhatsapp, instagram, messenger, web or other.
first_onlystringNotrue counts each person only in the bucket of their earliest-ever ad Touch. Default false.
limitstringNogroup_by=ad only: ads to return, 1 to 1000. Default 100.

Request

cURL
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

FieldTypeDescription
ad_peopleintegerDistinct people with at least one ad Touch in the bucket.
ad_touchesintegerAd Touches in the bucket. Equal to ad_people with first_only=true.
by_origin.partner_ad_referralobjectImported 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_referralobjectA WhatsApp or Instagram conversation Atribu received with Meta's ad referral.
by_origin.webobjectA tracked website visit whose link named the ad.
by_origin.otherobjectAny other ad-tagged Touch (a CRM conversion's own ad parameters, a store journey, a Touch inherited by a recurring payment).
meta_messaging_conversations_startedinteger | nullMeta'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.
totalsobjectThe whole window with each person counted once.
origin_labelsobjectEnglish 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.


On this page