Skip to main content

Ads console walkthrough: connect, campaign, CAPI, attribution

This page walks the Ads console operation by operation: connect a Meta Ad Account over OAuth, create click-to-chat campaigns into PAUSED, verify Conversions API (CAPI) delivery with a test event, post real conversions, ingest Lead Ads form submissions, and read attribution over the API. The endpoint schemas live in the Ads API reference; the attribution model is ads attribution and lead ingestion (concepts); the deliberate scope boundary is Ads: feature boundaries. If you want the pre-computed linear version of the same loop (one lead, one conversion, one ROAS read), see your first attributed lead. One correction to how you point the page: Attribution is endpoint-only — the Ads console has four tabs (Ad Campaigns, Conversions, Audiences, Lead Ads). The console deliberately does not re-derive what the API computes; point your own dashboard at the endpoint and render what it returns.

Where it lives

Open Outbound > Ads (Ads is a tile on the Outbound hub). The page splits into four tabs, and the campaigns tab doubles as the connect surface: There is no Attribution tab — the readout is endpoint-only. The tiles map with the question each tile answers is Ads: feature boundaries.

Object-verb map

Everything the Ads surface does, one line per operation: Role map: every authenticated role reads status, campaigns, audiences, and attribution. Writes — connect/disconnect, campaign create/pause/resume/delete, audience create/sync/delete, conversions configuration/test/post, lead ingestion — need owner or admin.

Connect a Meta ad account over OAuth

One ad account connects per organization; the connection records its billing currency so budgets and spend render in that currency. Meta Ads is early-access — you can connect and launch today.
1

Open the connector card

In Settings > Channels, find the Meta Ads card and choose Connect via Settings. The Ads page links there directly.
2

Authorize with Meta

Sign in with your Facebook profile and pick the ad account in Meta’s account picker. Orbit exchanges the OAuth code for a token, upgrades it to a long-lived token, takes the first ad account, and encrypts it at rest.
3

Confirm the linked account

Back on the Ads page, the connect banner turns green and names the connected account id. Every other workspace member with a role can then read campaigns and audiences; only owner/admin can write.
The Ads page connect card lists the same steps (go to Settings > Channels, log in with Facebook via OAuth, select the ad account, return to create campaigns). One account per org, in practice: the connect step replaces rather than accumulates — a second OAuth authorization against a different ad account overwrites the stored connection, so every campaign, dataset, and attribution window immediately follows the new account. There is no multi-account list to scope a campaign to. If your organization bids from several ad accounts, run each account under a separate Orbit organization. Admins joining later inherit whatever account is connected; to switch, either run connect again with the other account or use Disconnect first.

Disconnect

The Disconnect button on the green banner opens a confirmation dialog that asks you to type DISCONNECT — all-caps, case-sensitive — before the button enables. The dialog states plainly: “This stops live data flow from your Meta Ad Account and disables click-to-chat campaigns. You’ll need to re-authorize with Meta to reconnect.” The console gates disconnect behind a typed phrase on purpose: it deletes the stored connection and requires a fresh OAuth authorization to redo.

Create a campaign — PAUSED at creation

On Ad Campaigns > Create campaign, fill:
  • Campaign Name — required; placeholder suggests something like “Summer WhatsApp Leads”.
  • Daily Budget — required, number ≥ 1 with 0.01-step increments, in the ad account’s billing currency (the field label carries the account currency code, e.g. (EUR)).
  • Destination — WHATSAPP (WhatsApp), MESSENGER (Messenger), INSTAGRAM_DIRECT (Instagram Direct). These are the only three destination values the API accepts.
  • Ad Text — required (the ad copy Meta serves).
  • Image URL — optional; must be a blank-or-safe-HTTPS URL server-side.
  • Welcome Message — optional; the greeting text the click-to-chat entry shows.
You can also set an objective (OUTCOME_LEADS default, or OUTCOME_TRAFFIC / OUTCOME_ENGAGEMENT) and targeting (countries[], age bounds 18–65) over the API; the console form keeps the four fields above required. New campaigns are always created with status: "PAUSED" — a server-side default you cannot override at create time (the console never sends a status). That is deliberate: review the campaign in Meta before spending, then flip it active from the campaign row menu with Resume. Because it starts PAUSED, Meta-side ad review has already run by the time delivery begins. Row menu actions per status: Pause, Resume, View Insights (per-campaign funnel — impressions, reach, clicks, CTR, spend, cost per click, conversations rendered at the currency’s precision), and Delete. Delete is permanent at Meta as well as here.

Audiences: create, sync, delete

The Audiences tab activates a CDP segment as a hashed Meta Custom Audience — for targeting, or as a suppression list. Email and phone are SHA-256 hashed before they leave Devotel; the same hashing layer both directions (audience sync + conversion match keys).
  • Create audience — name, optional description, optional linked CDP segment (search segments by name; choose No segment (link later) to create empty). Server-side you can create with or without a segment_id.
  • Sync members — a dialog with two modes: Add members (targeting) and Remove members (suppression). Choose Sync now. The sync pages contacts 10,000 per Meta call and caps at 100,000 members; past the cap it returns truncated: true so you know the membership did not fully ship.
  • Delete — removes the Custom Audience from Meta and removes the segment link. The confirmation warns this cannot be undone.
Syncing against a segment with no email/phone contacts ships an empty payload — the API filters segment membership down to rows with a contactable identifier before calling Meta.

CAPI: test-then-post loop

The conversion path has three cards, in this order:
  1. Configuration — Dataset ID. Enter the Meta dataset id (the id lives under the centralized Meta connection settings). Save it here; on the wrong card, Send Test Event rejects with CAPI not configured (the console surfaces the failure message, and the API returns Conversions API not configured. Connect your Meta Ad Account and set a dataset ID.).
  2. Test event. The Send Test Event button posts a sample Lead event. The Recent Test Events panel is a session-scoped accept log — the ten most recent sends that Meta confirmed with {count} received. It only logs a row when Meta acknowledged events_received > 0; a rejected send never becomes a false green. Each accepted entry also shows Meta’s fbtrace id when the response carried one. Fire the test path before live traffic on every campaign.
  3. Post conversion. The form asks for an event name — Lead, Purchase, CompleteRegistration, Schedule, Contact, CustomizeProduct, ViewContent, or any Meta standard event up to 64 characters — plus at least one match key: a ctwa_clid click id or a phone number (email and an external id are also accepted by the API). Optionally add value (a non-negative number) and currency (a 3-letter ISO code; the form uppercases it before posting). The button stays disabled until a match key is present, because without one Meta cannot attribute the event. Phone, email, and external id are SHA-256 hashed server-side before they leave; the ctwa_clid travels as-is (it is Meta’s own click id, deterministic).

The deterministic join vs. the probabilistic one

When a conversion posts with the ctwa_clid from the inbound click-to-chat message, Meta joins it to the entry ad deterministically. When it posts with only a hashed phone/email/external id, Meta matches it to an ad impression probabilistically — iOS privacy and tracking-protection knockouts reduce this rate on Meta’s side. The console prompts you toward the ctwa_clid whenever you have one and the attribution model records a much stronger match. Lost match expectation: some phone/email-only conversions land unattributed on Meta’s side — the click id survives, the hashed identity does not always.

Dedup between pixel and server events

If you also run a browser pixel elsewhere, keep the event id stable across both paths: Meta dedups same-name same-id events server-side vs pixel-side. Orbit only ever sends server-side, so pixel-vs-server duplication never originates from this surface.

Route webinars: Lead Ads ingestion

The Lead Ads tab ingests Meta lead-ad form submissions by pasting the leadgen webhook payload your webhook endpoint received. Paste the exact envelope:
Only leadgen changes are extracted; each leadgen_id is fetched from Meta’s Graph (fields: field_data, created_time, ad_id, ad_name, adset_id, campaign_id, campaign_name, form_id), then mapped onto contacts with email and/or phone and upserted through the same dedupe-consent-audit path as a CSV import. A lead with neither email nor phone is skipped (skipped_no_contact). Import leads returns a tally: Received, Fetched, Created, Updated, Duplicates, Skipped (no contact info), and Fetch failures — per-lead failures never abort the batch. Lead Ads is a second entry path only: it lands in the same attribution view as click-to-chat entries. ctwa_clid matching does not apply to lead-form entries (there is no ctwa_clid to reference), so those leads attribute through the hashed identity match.

Attribution: the API-only tab

Attribution is the roll-up the API endpoint GET /ads/ctwa/attribution computes. There is no Attribution tab in the Ads console — point your own dashboard at the endpoint and render what it returns. The one query parameter is window_days (integer, 1–365, default 30): entry points older than the window do not count, and Meta ad spend is fetched over the same window so the ROAS numerator and denominator align.
Swap the window to compare the standard lookbacks — $window is 1, 7, or 30 for the yesterday/week/month presets (any integer 1–365 works):
The report returns reporting_currency (your ad account’s billing currency), window_days, 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 ads[] — one row per entry ad with 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; per-ad contact counts then stay partition-clean (summing across ads never double-counts). Revenue in a currency with no caller-supplied FX rate surfaces in unconvertible_revenue_cents rather than blending at 1:1. cost_status is exactly ok or no_spend — when no account is connected or spend fetch failed, the report still returns entry attribution with roas: null, spend_available: false, cost_status: "no_spend".

The full loop in one read

Closed loop, end to end — the six operator steps from ad dollar to attributed ROAS:
  1. Connect — authorize one Meta Ad Account over OAuth (Settings > Channels > Meta Ads); the connect banner goes green and names the account id.
  2. Create the campaign — Ad Campaigns > Create campaign with a WhatsApp, Messenger, or Instagram Direct destination. It lands PAUSED; review it in Meta Ads Manager, then flip it live with the row-menu Resume.
  3. A click lands as a conversation — a customer taps the ad; the entry message arrives in your Inbox already carrying the Meta referral stamp (the ctwa_clid and the entry ad id), so the entry is attributed the moment it arrives.
  4. The outcome posts back — when the conversation converts, post the business outcome (Lead, Purchase, …) through Conversions > Post Conversion with that ctwa_clid as the match key — a deterministic join back to the entry ad. Meta bids toward more of those clicks.
  5. Attribution closes the loop — read GET /ads/ctwa/attribution?window_days= (first-touch; the earliest entry fixes the ad a contact belongs to). Per-ad entry counts, attributed revenue, and Meta spend over the same window divide out to ROAS; a campaign that earns more than it spent shows roas > 1 and positive roi_pct on its ad rows.
That is the whole cycle — ad dollar → inbound conversation → posted outcome → attributed ROAS. The field-level mechanics of the referral stamp and first-touch model are in ads attribution and lead ingestion (concepts); this section is the operator’s-eye sequence, not the model spec.

Troubleshooting

  • Connect banner shows an error card instead of an account. The connection-status read failed (401/403/500 from /ads/status); the console surfaces “Couldn’t load Meta Ad Account status. Try again.” with a retry control. Fix the read before re-running OAuth — re-authorizing against a failing read recreates the same 500.
  • OAuth token expiry / revoked access. The banner still names the account, but campaign syncs and conversion posts start failing because the stored token can no longer be used (expired, the Meta user de-authorized the app, or the account’s access changed). In the Meta app yourself, a revoked grant also shows under your Facebook profile’s Business Integrations. The fix is the same as a fresh connect: re-run the OAuth flow from Settings > Channels > Meta Ads — the stored token upgrades to a new long-lived one. Campaigns, audiences, and the dataset survive a re-connect untouched; only the credentials change.
  • Disconnect button greyed out despite typing. The required phrase is DISCONNECT — all-caps, case-sensitive. Typing disconnect does not enable the button.
  • Send Test Event rejected with CAPI not configured. The dataset id is unset or saved on the wrong card. Save it under Conversions > Configuration exactly, then resend.
  • Meta returns HTTP 200 with zero received events. A silent reject — the dataset is paused or misconfigured at the Meta side. The accept gate catches it: the toast flags the failure and Recent Test Events does not log it. Note the API boundary conversationally: CAPI errors from Meta surface to you as a generic 500 with only the serving-side digest; the raw Graph error string is logged server-side and to Sentry, which is why the test-then-post loop is the ground truth, not the HTTP status.
  • Post Conversion stays disabled. Provide at least one match key (ctwa_clid or a phone) so Meta can attribute the event.
  • ctwa_clid match failures. Capture the click id from the inbound message at entry time — the console cannot reconstruct it later. An event posted with only a hashed phone/email tells Meta the outcome happened but gives it no entry ad to credit. First-touch attribution also keeps a contact on the earliest ad; a later click on a different ad does not re-attribute the contact.
  • Lead-ad entries show no ctwa_clid. Lead Ads instant-form submissions have no click id to reference — a form entry never carried one — so those leads attribute through the hashed email/phone identity match, the probabilistic join. This is expected, not a broken import: expect a weaker Meta match rate on form entries than on click-to-chat entries, and prefer the click-to-chat entry path where attribution certainty matters.
  • Value entered but rejected. value must be a non-negative number; currency, when set, must be a three-letter ISO code — the form uppercases before posting.
  • Audience sync reports truncated. Membership exceeded the 100,000-member cap. Narrow the segment and re-sync.
  • Lead Ads import skips leads. A lead with no email and no phone is skipped — check the field mapping in your form builder at Meta before re-importing.

Privacy guardrails: hashed PII and what leaves

Match keys you post — phone, email, external id — are SHA-256 hashed server-side before they leave Devotel; the ctwa_clid travels as-is because it is Meta’s own click id. Audience sync applies the same hashing layer to phone/email membership. Attribution reads only the referral blocks on inbound messages — no raw PII crosses either direction. These are tenant-owned controls: Orbit is the conduit, and consent to use a contact’s data in ad platforms sits on your side of the consent line — keep your audience source segments aligned with the audience consent inspector and opt-out lists.

Scope and boundary guardrails — what NOT to build here

The Ads console is deliberately scoped to Meta click-to-chat only:
  • Non-Meta networks. No Google Ads, no TikTok Ads, no X/LinkedIn/Snapchat — none of them expose the deterministic ctwa_clid join, and folding a weaker attribution in would be a downgrade. Any new network ships as its own connector with its own documentation, announced on the changelog.
  • Creative / media-buying work. The console manages the campaign loop (create, pause/resume, delete, audiences, conversions, attribution) — not creative production, ad-set-level trade-craft, bidding strategy, or account-level Meta Ads Manager work. Those stay on Meta’s side.
  • Lead Ads is form ingestion only. A lead-form submission lands in the same attribution view as a click-to-chat entry — it is ingestion of a Meta-native form, not a general lead-capture builder for other networks.
The pointer page is Ads: scope and feature boundaries — this walkthrough deliberately does not duplicate that boundary drawing.

See also