Skip to main content

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:
A working connection returns the account id and 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 the ctwa_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:
The extraction is deliberately narrow: only 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 live segment_id, and auto_refresh so membership tracking survives launches. Size the filter before saving it:
The response returns 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:
Two modes: 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.
Either path writes the same attribution anchor. Keep the 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, or PUT /ads/conversions/dataset), then fire the test path before live traffic:
Only after the test panel confirms a received row, post the real event:
The event needs at least one match key — the 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:
Expected response shape: 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_UNAVAILABLE on every call. No connection exists. Connect (Section 1) and re-read /ads/status before anything downstream.
  • 422 on the conversion post. No match key — supply ctwa_clid, or a phone/email/external_id in user_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 inside window_days. Check the campaign status and widen the window (1–365; default 30).
  • truncated: true on the audience sync. Membership exceeded the 100,000-member cap. Narrow the segment and re-sync.

See also