Your first attributed lead, end to end
This guide walks the full Meta ads loop in order, once, from zero to a readable attribution row: connect the account, register how leads arrive, build a segment, launch a campaign, sync the audience, ingest one entry, forward one conversion, and read ROAS back. The Ads hub overview explains what the hub owns; the console walkthrough details each tab; the API-driven loop covers the same ground over curl. This page is the linear first run through all of it — open one section in one sitting. Prerequisites: owner or admin role on your API key (every write below needs it), and a Meta Business account with an ad account and a Facebook app you can authorize.1. Connect the Meta ad account
Open Settings > Channels > Meta Ads and choose Connect. Sign in with Facebook, pick the ad account in Meta’s picker, and Orbit exchanges the OAuth code for a long-lived token, encrypts it at rest, and records the account’s billing currency. One ad account connects per organization. Back on the Ads page, the connect banner turns green and names the connected account id. Validate the link over the API:currency. Until it does, every other Ads endpoint returns CHANNEL_UNAVAILABLE — fix this before anything downstream.
Disconnect is behind a typed phrase on purpose: the banner’s Disconnect dialog asks you to type DISCONNECT (all-caps, case-sensitive) before the button enables, because it stops live data flow from Meta and disables campaigns. There is no accidental disconnect, and no silent one.
2. Register how leads arrive
Leads enter two ways. Register the path your campaign uses: Path A — click-to-chat. The contact taps the ad into a WhatsApp, Messenger, or Instagram Direct conversation. The first inbound message carries Meta’s referral block — the ad id and thectwa_clid click id — which becomes the attribution anchor. Nothing to register; the inbound message carries the anchor. Capture ctwa_clid at entry time, because the console cannot reconstruct it later.
Path B — lead-form ingestion. Meta Lead Ads forms arrive on your webhook endpoint, and you paste the envelope into the Lead Ads tab:
leadgen changes are pulled from the envelope, each leadgen_id is fetched from the Meta Graph (fields field_data,created_time,ad_id,ad_name,adset_id,campaign_id,campaign_name,form_id), and the contact is upserted through the same dedupe-consent-audit path as a CSV import. A lead with neither email nor phone is skipped (skipped_no_contact), and per-lead fetch failures tally into the result without aborting the batch. Path B leads attribute later through hashed identity — no click id ever existed for them.
3. Build the segment and preview its size
Build the audience before any spend. The recipe lives in CDP audiences: build segments; for ads, two properties carry the weight: a filter over facts (attributes, events, computed traits) so the sync references a livesegment_id, and auto_refresh so membership tracking survives launches.
Size the filter before saving it:
match_count — sizing only, nothing persisted. A 0-match filter that reaches spend is a paid no-op; preview first.
4. Create the campaign — PAUSED first, ACTIVE after review
On Ads > Ad Campaigns > Create campaign, set the name, daily budget (rendered in the account’s billing currency), destination (WHATSAPP, MESSENGER, or INSTAGRAM_DIRECT — the only three values), and ad text. Optional over the API: objective (OUTCOME_LEADS default), countries[], and age bounds.
New campaigns are created PAUSED — a server-side default you cannot override at create. Review the campaign in Meta, then flip it to ACTIVE from the campaign row’s Resume action. Because Meta ad review runs while the campaign is paused, a clean review means delivery starts ordered, not retro-fixed.
5. Sync the segment as a Custom Audience
On Ads > Audiences, create an audience linked to the segment, then Sync now:add ships membership for targeting; remove suppresses those contacts. Email and phone are SHA-256 hashed before they leave. The sync pages 10,000 contacts per Meta call and caps at 100,000 members; past the cap the response marks truncated: true — narrow the segment and re-sync. Before a segment feeds an ad audience, run the audience consent inspector against it — consent to use a contact’s data on ad platforms sits on your side of the line.
6. Ingest one entry
The attribution anchor lands when a contact enters. Two ways that first row writes:- Click-to-chat — the contact taps the campaign ad; the first inbound WhatsApp/Messenger/Instagram Direct message carries the referral block with the ad id and
ctwa_clid. Entry records at that moment. - Lead form — the contact submits the Lead Ads form; paste the envelope and ingest (Section 2). Entry records at ingest.
ctwa_clid if one exists — it is the deterministic join the conversion below closes on.
7. Forward one conversion
Set the dataset id once (Conversions > Configuration, orPUT /ads/conversions/dataset), then fire the test path before live traffic:
received row, post the real event:
ctwa_clid, or a hashed phone, email, or external_id. Without one, Meta cannot attribute the event and the API rejects with 422. The click id joins deterministically; a hashed-identity join is probabilistic on Meta’s side, so prefer the click id when you captured one at entry (Section 6). Phone, email, and external id are SHA-256 hashed server-side before they leave; the ctwa_clid travels as-is, because it is Meta’s own click id.
8. Read it back
The Ads console deliberately has no attribution tab — the API is the report. Read it:reporting_currency, window_days, totals (total_entry_count, total_unique_contacts, total_attributed_revenue_cents, total_unconvertible_revenue_cents, total_spend_cents, roas, roi_pct, cost_status, spend_available), and an ads[] array with one row per entry ad: ad_id, first/last seen, entry_count, unique_contacts, attributed_revenue_cents, unconvertible_revenue_cents, spend_cents, roas, roi_pct, cost_status.
Attribution is first-touch: the earliest captured referral fixes the entry ad, and per-ad contact counts partition cleanly. Revenue in a currency with no caller-supplied FX rate surfaces in unconvertible_revenue_cents rather than blending at 1:1. And when no spend is fetchable — no account connected, or the Meta spend read failed — the row still returns entry attribution with roas: null and cost_status: "no_spend", so the window never silently mixes numerator and denominator periods.
9. Troubleshooting
CHANNEL_UNAVAILABLEon every call. No connection exists. Connect (Section 1) and re-read/ads/statusbefore anything downstream.- 422 on the conversion post. No match key — supply
ctwa_clid, or aphone/email/external_idinuser_data. The console’s Post Conversion button stays disabled until one is present. - Empty-segment sync. The API filters segment membership to rows with an email or phone before calling Meta; a segment over filters that match no identifier ships an empty payload. Re-check the segment filter against the preview (Section 3).
- Test event logs nothing. The recent-test-events panel only logs sends Meta confirmed with
events_received > 0; a silent reject never becomes a false green. If the test event posts but nothing logs, check the dataset id at Conversions > Configuration. - Attribution row is empty. The campaign never reached
ACTIVE, or no entry landed insidewindow_days. Check the campaign status and widen the window (1–365; default 30). truncated: trueon the audience sync. Membership exceeded the 100,000-member cap. Narrow the segment and re-sync.
See also
- Ads hub overview — what the hub owns and which guide answers which job
- Ads API reference — endpoint schemas for every call above
- Ads attribution and lead ingestion (concepts) — the first-touch model this loop implements
- CDP audiences: build segments — the segment recipe
- Audience consent inspector — verify consent before a segment feeds an ad audience