Conversions API
The Conversions API (CAPI) lets Meta see conversions that happen outside the browser — orders confirmed on your server, leads qualified in a CRM, purchases in a physical store. AdWitch hosts the integration for you: you send events, AdWitch validates them, normalises and SHA-256-hashes the match fields Meta expects hashed, deduplicates them, and forwards them to your pixel.
Two ways in:
- Hosted ingest endpoint — one URL and key per ad account; your backend or CRM sends events to it in the format below.
- Chat upload — “upload these purchases as offline conversions” with a table or CSV; the agent hashes the rows and sends them after your confirmation.
Both need a default pixel on the ad account (Ad accounts at /accounts) and, on Meta’s side, the Business Tools Terms accepted by an admin of your Business.
Stores on Shopify, WooCommerce, BigCommerce, Wix or Tilda
Section titled “Stores on Shopify, WooCommerce, BigCommerce, Wix or Tilda”A store or site on one of these platforms does not need the ingest endpoint: the platform’s official integration already sends server events (Conversions API) to Meta next to the browser pixel. The Signal & optimisation event check on Ad accounts recognises these platforms by the domain the pixel fires on and shows the same steps when only browser events arrive — on Shopify, WooCommerce and BigCommerce, whose integrations also send customer information, when customer matching is weak too.
Shopify — Meta’s free Facebook & Instagram app:
- In Shopify admin, go to Sales channels → Facebook & Instagram → Settings → Data sharing settings.
- In the Customer data sharing section, turn on customer data sharing and choose the Maximum level (Conversions API + Meta pixel).
- Connect the Meta pixel that is bound as the ad account’s default pixel in AdWitch.
- Remove other pixel apps and any Meta pixel code added to the theme by hand, so events aren’t counted twice.
Details: Shopify’s help article.
WooCommerce — the official free Meta for WooCommerce plugin (formerly Facebook for WooCommerce):
- Install and activate the plugin from the WordPress plugin directory.
- Connect it to Meta and choose the Meta pixel bound in AdWitch.
- Keep the plugin’s Conversions API (server events) switched on.
- Remove other pixel plugins and any Meta pixel code added to the theme by hand.
BigCommerce — the built-in Meta Pixel integration, which at no extra cost also sends Conversions API events with hashed customer information (email, phone, name):
- In the BigCommerce control panel, go to Settings › Data Solutions and, under Web analytics, click Connect next to Meta Pixel.
- Click Connect with Meta Pixel, log in to Facebook and select your Business Manager, Facebook Page and the Meta pixel bound in AdWitch.
- On the What is BigCommerce allowed to do screen, leave all options enabled.
- Remove other pixel apps and any Meta pixel code added to the theme or scripts by hand.
Details: BigCommerce’s help article.
Wix — the built-in Meta Pixel & CAPI integration, which sends events from Wix’s servers. It needs a Premium plan, a connected domain and the domain verified with Meta:
- In the site’s dashboard, open Marketing Integrations and click Connect under Meta Pixel & CAPI. If only an older Meta Pixel integration is there, delete it, refresh the page and connect Meta Pixel & CAPI.
- Click Connect to Facebook, sign in and, when asked for the pixel, choose the Meta pixel bound in AdWitch.
- Remove any Meta pixel code added to the site by hand (custom code).
Details: Wix’s help article.
Tilda — the built-in Conversions API connection, which sends form submissions (Lead) and cart payments (Purchase) to Meta from Tilda’s server:
- In Meta Events Manager, open the Meta pixel bound in AdWitch, go to its settings and, in the Conversions API section, click Generate access token.
- In Tilda, go to Site Settings → Forms → Facebook Conversion API, paste the pixel ID and the token, and save.
- In the Content settings of every form whose submissions should reach Meta, the cart included, tick the Facebook Conversion API data receiver, then republish those pages.
- Remove any other Meta pixel code added to the site by hand.
Details: Tilda’s help article.
Sending the same purchases through the ingest endpoint as well? They count twice unless both paths send the same event_id (with the same event_name) for the same order.
Webhooks from these platforms aimed at the ingest URL don’t work: they aren’t in Meta’s server-event format, so the endpoint answers 400 DATA_REQUIRED (or 401 when the request carries no key) and records nothing. If such requests carry your ingest key, the Last problem note under Conversions API on the account card says so — naming the platform when the request shows which one — and links back to this section. Delete those webhooks in the platform and use its official integration above.
Setting up the ingest endpoint
Section titled “Setting up the ingest endpoint”- Open Ad accounts (
/accounts) and expand Conversions API on the account card. - Click Create ingest key. The key (
capi_<id>_<secret>, with a 12-character hexadecimal ID and a 48-character hexadecimal secret) is shown once — store it in your backend now; AdWitch keeps only a hash. Lost it? Rotate key issues a new one and kills the old one immediately. - Optionally set a Test event code (Events Manager → Test events). While set, every batch shows up in the Test Events tab instead of production reporting — clear it when you go live.
- Copy the curl or Webhook JSON example and send your first event.
The section also shows the last-24h counters (received, forwarded, rejected by validation, duplicates, rejected by Meta), recent batches and the last problem, if any. Accept events pauses the endpoint without revoking the key (senders get 403 CAPI_DISABLED); Revoke key stops it for good (401 KEY_REVOKED).
Only team owners and admins manage keys; other members see the status.
Ingest contract
Section titled “Ingest contract”POST https://api.adwitch.ai/api/capi/v1/eventsAuthorization: Bearer capi_<id>_<secret> (or X-Capi-Key: …)Content-Type: application/jsonThe body is the standard Meta server-event shape — data[] of events (a bare array is accepted too), plus an optional request-level test_event_code that overrides the one saved in settings:
{ "data": [ { "event_name": "Purchase", "event_time": 1757600000, "action_source": "website", "event_source_url": "https://shop.example.com/checkout/thank-you", "event_id": "order-10045", "user_data": { "em": "customer@example.com", "ph": "+34600000000", "client_ip_address": "203.0.113.5", "client_user_agent": "Mozilla/5.0", "fbp": "fb.1.1700000000000.123456789" }, "custom_data": { "value": 49.9, "currency": "EUR", "order_id": "10045" } } ], "test_event_code": "TEST12345"}Rules applied per event:
| Field | Rule |
|---|---|
event_name |
Required. Standard names (Purchase, Lead, CompleteRegistration, …) or a custom one. |
event_time |
Unix seconds (milliseconds and ISO dates are accepted). At most 7 days old, at most 10 minutes in the future. |
action_source |
website, email, app, phone_call, chat, physical_store, system_generated, other. website also needs event_source_url; app needs app_data (advertiser_tracking_enabled 0 or 1 and extinfo with the platform a2/i2 and the OS version) and counts only if the account’s pixel is the dataset linked to the app. business_messaging is refused here (business_messaging_not_accepted): those events belong in the dataset of a Page, WhatsApp Business account or Instagram account, not the pixel; send them from the chat (see below). |
event_id |
Recommended — it is your idempotency key. Missing → a deterministic id is derived from the event, so a retried request does not double count. |
user_data |
At least one matchable identifier (em, ph, external_id, fbp/fbc, lead_id, IP + user agent, or name + city/zip/birthday). Long aliases (email, phone, first_name, …) are understood. |
custom_data |
Purchase needs value and currency (the ad account’s currency is filled in when missing). |
Hashing. em, ph, fn, ln, ge, db, ct, st, zp, country and external_id are normalised (lower-case, trimmed, phone digits only with country code, …) and SHA-256-hashed by AdWitch. A value that already is a 64-hex SHA-256 digest is forwarded as is. client_ip_address, client_user_agent, fbp, fbc, lead_id and the other identifiers Meta expects in clear are forwarded unhashed.
Deduplication. The same event_name + event_id within 48 hours on the same account is dropped and reported under duplicates — this covers retries from your side and the case where the same orders arrive both through the endpoint and by chat upload.
Limits. Up to 1000 events and 1 MB per request; 600 requests per minute per key and 1,200 per minute per IP. Larger volumes: split the batch.
Responses
Section titled “Responses”202 Accepted — something was queued for Meta; 200 OK — nothing to forward (all rows rejected or duplicated). Both carry:
{ "status": "queued", "batch_id": 812, "pixel_id": "555000111", "received": 4, "accepted": 2, "rejected": [{ "index": 2, "reason": "event_time_invalid", "field": "event_time", "message": "…" }], "duplicates": [{ "index": 3, "reason": "duplicate_event_id", "field": "event_id", "message": "an event with the same event_name and event_id was already accepted in the last 48 hours" }], "accepted_event_ids": [{ "index": 0, "event_id": "order-10045" }, { "index": 1, "event_id": "order-10046" }], "hashed_user_data_keys": ["em", "ph"], "test_event_code": "TEST12345"}Rejected and duplicate rows include an index, reason and message, with a field when applicable — never the personal value. If nothing was queued, status is nothing_to_forward and batch_id is null. test_event_code appears only when set.
| Status | reason |
Meaning |
|---|---|---|
| 401 | KEY_MISSING / KEY_INVALID |
No key, unknown key or wrong secret. |
| 401 | KEY_REVOKED |
The key was revoked in settings; create a new one. |
| 403 | CAPI_DISABLED |
“Accept events” is off for this account. |
| 409 | PIXEL_NOT_BOUND |
The account has no default pixel any more. |
| 409 | FB_NOT_CONNECTED |
The account’s Meta connection is gone; reconnect it. |
| 400 | DATA_REQUIRED / TOO_MANY_EVENTS |
Missing data[] or more than 1000 events. A body that isn’t in Meta’s format also gets hint and docs, plus platform and topic when it is a webhook from one of the platforms above. |
| 429 | RATE_LIMITED |
Slow down; Retry-After tells you when. |
Forwarding is asynchronous: the batch status (forwarded, partial, failed, terms_required) and Meta’s per-event warnings appear in the account’s Recent batches. If Meta refuses because the Business Tools Terms are not accepted, the section shows the notice with the Business link; nothing is retried until an admin accepts them.
Uploading conversions from the chat
Section titled “Uploading conversions from the chat”Ask the agent, for example: “Upload these purchases as offline conversions” and paste a table or attach a CSV with columns like email, phone, order_id, value, currency, date (English, Russian and Spanish headers are recognised). The agent:
- normalises and hashes the rows locally and reports what would be sent (
dry_run) — how many rows, which identifiers were hashed, which rows are rejected and why; - asks for your explicit confirmation — customer data (as hashes) leaves the platform;
- sends in batches of up to 1000 events per Graph call and reports
events_receivedplus Meta’s warnings; - points you to Events Manager → Test events (with a test event code) or Overview;
get_pixel_diagnosticsshows the events arriving.
Chat uploads share the 48-hour deduplication window with the ingest endpoint.
Sales closed in Messenger, WhatsApp or Instagram chats
Section titled “Sales closed in Messenger, WhatsApp or Instagram chats”Purchases and qualified leads won in a conversation started by a click-to-message ad go through Meta’s Conversions API for Business Messaging. Meta can then optimise click-to-Messenger and click-to-WhatsApp ads for purchases made in chats (the purchases in messages goal); Instagram events are for measurement only. Each row needs messaging_channel (whatsapp, messenger or instagram) and that chat’s pair of identifiers:
| Channel | Account | Person in the chat |
|---|---|---|
whatsapp |
whatsapp_business_account_id |
ctwa_clid (from the click-to-WhatsApp webhook) |
messenger |
page_id |
page_scoped_user_id (PSID) |
instagram |
ig_account_id |
ig_sid (IGSID) |
Supported events: Purchase, LeadSubmitted, QualifiedLead, InitiateCheckout, AddToCart, ViewContent, OrderCreated, OrderShipped, OrderDelivered, OrderCanceled, OrderReturned, CartAbandoned, RatingProvided, ReviewProvided. One upload covers one channel and one account. The events go to that Page’s, WhatsApp Business account’s or Instagram account’s dataset (created if missing), never to the pixel. A row without a channel, with an unknown channel or with a missing or mismatched identifier is rejected with its reason code. Before the first send, the agent lists the extra Meta permissions (page_events for Messenger; whatsapp_business_management and whatsapp_business_manage_events for WhatsApp; instagram_manage_events for Instagram) with their status, and sends nothing while one of them is known to be missing. Meta does not deduplicate these events, so upload each sale once.
Sales stages from a CRM
Section titled “Sales stages from a CRM”The Conversion leads optimisation goal (QUALITY_LEAD, Meta’s “Maximise number of qualified leads”) learns from the stages your CRM records for each lead. Send them to the ingest endpoint in Meta’s CRM format — one event per stage change, named after the stage:
event_name— the stage as your CRM names it (Lead,Qualified,Opportunity,Closed Won, …), the first lead stage included.action_source—system_generated.user_data.lead_id— the Meta lead ID of an instant-form lead (Meta’sleadgen_id) as a string, exactly as Meta gave it; it is forwarded unhashed. No lead ID: send the email or phone used for matching, which AdWitch hashes.custom_data.event_source—crm;custom_data.lead_event_source— your CRM’s name (HubSpot,Salesforce,In-house CRM, …).event_time— the stage time: at most 7 days old and after the lead.
{ "data": [ { "event_name": "Qualified", "event_time": 1757600000, "action_source": "system_generated", "event_id": "hubspot-1234567890123456-qualified", "user_data": { "lead_id": "1234567890123456" }, "custom_data": { "event_source": "crm", "lead_event_source": "HubSpot" } } ]}Keep IDs in quotes: a 15–17-digit lead ID written as a JSON number can be rounded by the tools it passes through and then points to another lead.
The 7-day rule. Meta takes a stage only if its event_time is at most 7 days old, so AdWitch rejects older ones with event_time_too_old — a monthly CRM export loses most of its stages. Upload stage changes at least daily: first with a test event code (Events Manager → Test events), then without it.
In the chat, attach the stages as a table or CSV with the columns lead_id, event_name, event_time, event_source and lead_event_source and ask the agent to send them as CRM stages (action_source system_generated).
What is not included
Section titled “What is not included”- Hosting the browser pixel snippet or a JavaScript SDK.
- The SDK’s App Events API (app events from your own server use
action_sourceapp, above), catalog feeds and the legacy Offline Conversions API (offline event sets). - Our own store app or plugin for Shopify, WooCommerce or another store platform — use the official integrations described above.
Events are counted per team in the usage log; they are currently not billed.