DEV Community

Aleksander Sekowski
Aleksander Sekowski

Posted on

Pixel Helper cannot see a Conversions API POST. A 200 is not the contract.

A checkout worker posts a Purchase to the Meta Conversions API. The response is HTTP 200. Test Events stays empty. The next move is usually a new access token.

The token is rarely the bug. Pixel Helper cannot see the POST at all. The 200 is liveness. The body is the contract.

Meta Pixel Helper, TikTok Pixel Helper, and GTM preview watch the browser. Conversion API events are POSTs from your backend, from server-side GTM, or from a queue worker. They do not appear in Chrome Network unless you proxied them through the page. Debugging a server event inside Pixel Helper is how you conclude the server is down when event_time had thirteen digits. A CAPI tester is the check on the JSON you are about to ship. HTTP 200 is not validation is why the status code is the wrong assert. Conversions API not working is the ticket this produces.

What the tester checks, and what it refuses to pretend

A conversion API validator checks required fields, types, timestamp units, hashed versus raw identifiers, action_source enums, event_source_url when the source is the web, and value plus currency on money events. Each finding has a stable id, a severity, a fix, and a link to the page the vendor published.

It does not prove the event attributed. It does not log into Events Manager. It does not mint a click id you never stored. It does not call the Graph API. A pass means the artifact matches the published envelope. Attribution is a later, slower UI. Use Pixel Helper for the browser pipe: event name, eventID, value, whether two base codes fired PageView. Use the validator on the JSON. Use Test Events only after the body already passed, and strip test_event_code before production. That field diverts the event into the test panel. Leaving it on a live POST is how a green QA panel hides a pipe that never trains.

The usual first body looks like this. It 200s. It should not ship.

{
  "data": [{
    "event_name": "Purchase",
    "event_time": 1770000000000,
    "action_source": "website",
    "user_data": { "em": "buyer@example.com" }
  }]
}
Enter fullscreen mode Exit fullscreen mode

event_time is thirteen digits. Meta documents Unix seconds, ten digits, and a window of about seven days. A millisecond value lands far in the future. em is a raw address. Meta wants SHA-256 of a normalized email, and wants client_ip_address and client_user_agent in the clear. action_source is website with no event_source_url. Purchase has no custom_data.value and no custom_data.currency. The Meta Conversions API pack prints those as separate findings. A legal Purchase is event_name Purchase, a ten-digit event_time, the same event_id the pixel sent as eventID, event_source_url of the thank-you page, hashed email, plaintext IP and user-agent, and a numeric value with an ISO 4217 currency.

Other vendors document the same quiet drop

PostHog capture returns HTTP 200 when event or distinct_id is missing, and does not ingest the row. vendor.posthog.body.event.missing and vendor.posthog.body.distinct_id.missing exist because that drop is in the docs. timestamp must be ISO 8601. An epoch is read as ingestion time (vendor.posthog.body.timestamp.invalid).

Segment's HTTP Tracking API returns 200 for a track call that has no event name (vendor.segment.body.track_requires_an_event_name). Every call needs userId or anonymousId (vendor.segment.body.call_needs_an_identifier). timestamp is ISO 8601. A retry loop that only retries non-2xx will never see the miss.

GA4 Measurement Protocol collect returns HTTP 204. That means accepted for processing, not that the purchase had items. vendor.google-analytics.body.purchase_requires_ecommerce_fields is currency, value, transaction_id, and items. timestamp_micros at thirteen digits is milliseconds, not microseconds (vendor.google-analytics.body.timestamp_micros.invalid). The 204 will not mention it. /debug/mp/collect is where Google talks back. Production collect will store a misspelled event name as a custom event and move on. Use the debug endpoint in CI, then send the same body to production without debug_mode.

Meta often returns 200 with an error object in the JSON, or accepts the batch and reports per-event errors later. Read the JSON. A gateway 200 on a proxy in front of the vendor is even less information: nginx was up. access_token missing is vendor.meta-conversions-api.param.access_token.missing if you lint the request. If you only log status codes, a 200 with messages[].error is a silent miss. TikTok and Snap behave like other high-throughput ingest. Do not assume a 2xx mapped every field.

The clock and the enum are why one JSON cannot travel

Store one UTC instant. Convert at the edge. The field name is not a hint you can trust across companies.

Meta event_time and Pinterest event_time are Unix seconds, ten digits. Reddit CAPI v3 event_at is milliseconds, thirteen digits. LinkedIn conversionHappenedAt is milliseconds, inside about ninety days. A ten-digit clock on LinkedIn reads as 1970. GA4 timestamp_micros is about sixteen digits. TikTok's current Events API path wants seconds on event_time; the older pixel path still wants ISO 8601, and an epoch there is treated as arrival time. OpenAI Ads timestamp_ms is milliseconds. Date.now() is right for one of these and wrong for the others.

action_source is an enum per vendor, and there is no default you can omit and hope. Meta: website, app, phone_call, and the rest of that list. website requires event_source_url. Pinterest is web, not website. Snap is WEB. Reddit v3 is WEBSITE. Bare JSON has no host, so the Meta, Snap, and Pinterest packs tell each other apart by that field. A missing or misspelled action_source matches more than one pack, and each reports it. That is why a typo looks like three vendors yelling at once. Pin --rulepack so a Meta body does not collect Snap findings because the enum was omitted.

$ pixellint validate json @capi.json --rulepack vendor/meta-conversions-api
$ pixellint validate json @tiktok.json --rulepack vendor/tiktok-events-api
$ pixellint validate json @reddit.json --rulepack vendor/reddit-conversions-api
Enter fullscreen mode Exit fullscreen mode

What to keep next to the worker

A legal Meta Purchase. A legal Meta Lead without a fake value. A Purchase that must fail because event_time has thirteen digits. A Purchase that must fail because em is raw. A production fixture that must not contain test_event_code. A staging fixture that must contain it. A TikTok CompletePayment with the clock that path documents. A Reddit v3 event with a thirteen-digit event_at and WEBSITE. A LinkedIn conversion with the URN and conversionHappenedAt in milliseconds.

Dashboards lag, and they graph the wrong name. Purchse still charts. It charts as a custom event. Absence in the UI is not a 404. Presence in the UI is not proof the payload matched the contract you intended. CI on the JSON is the fast signal. Events Manager is the slow one. When someone files "CAPI not working," the first reply is the redacted JSON and the linter output, not a token rotation.

In the playground, set the format to JSON and paste the body the worker posts. Deep-link with ?kind=json. Redact access tokens and raw emails first. A 64-character hex digest is fine to keep. Artifacts you test on pixellint.org may be stored. Local cargo install pixellint, npm install pixellint, and cargo install pixellint-mcp do not send the payload. Same rule ids in all three, so a finding in QA is the same ticket in CI.

I maintain Pixellint. It is independent of Meta, TikTok, Reddit, Segment, and PostHog. A worker or a model can emit a body that the vendor accepts. The package is the check on that body before you call the endpoint. Hashing is the other half of the same payload: which fields are digests, and which must stay plaintext.

Top comments (0)