pixellint

CAPI · engineers · marketers · PMs

Check the CAPI body before Test Events

Short answer

Pixel Helper cannot see a server POST. Test Events is slow and still a vendor UI. A conversion API validator is a contract check on the JSON you are about to ship: required fields, timestamp units, hashed versus raw, ISO 4217 currency.

Validate this snippet Open the rulepack

What the validator actually checks

Required fields, types, timestamp units, hashed versus raw identifiers, action_source enums, event_source_url when the source is the web, value and currency on money events. That is the documented contract. Pixellint rulepacks encode those rules from vendor docs, each finding with a stable id, a severity, a fix hint, 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. Passing the validator means the artifact matches the published envelope. Attribution is a later, slower UI. See HTTP 200 is not conversion validation.

Pixel Helper cannot see CAPI

Meta Pixel Helper, TikTok Pixel Helper, and GTM preview watch the browser. Conversion API events are POSTs from your backend, sGTM, or a queue worker. They will not appear in Chrome Network unless you proxied them through the page. Debugging CAPI in Pixel Helper is how you conclude the server is down when the body was wrong.

Use Pixel Helper for the browser pipe: event name, eventID, value, whether two base codes fired PageView. Use a conversion API validator on the JSON. Use Test Events to confirm the vendor received a body that already passed the contract. That is three tools, three jobs. Mixing them is how a 13-digit event_time lives for a week while ROAS looks a bit off. See Facebook Pixel Helper vs Network for the browser debugger.

Paste the payload

In the playground, set kind to JSON and paste the body your worker posts. Deep-link with ?kind=json&a= plus the artifact. Redact access tokens and raw emails first; hashed em is fine to keep as a 64-character hex. Artifacts you test on pixellint.org may be stored; see privacy. Local CLI and npm do not send the payload anywhere. cargo install pixellint-mcp does not either.

The fixture below is the usual first bug: Date.now() in event_time, a raw email, website without a page URL, Purchase without money fields. It 200s. It should not ship. After you fix it, the same paste path is how you check a Lead, a refund, and a TikTok CompletePayment.

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

The clock map

Meta event_time and Pinterest event_time: Unix seconds, 10 digits. Reddit CAPI v3 event_at: milliseconds, 13 digits. LinkedIn conversionHappenedAt: milliseconds, 13 digits. GA4 timestamp_micros: microseconds, 16 digits. TikTok timestamp: ISO 8601. OpenAI Ads timestamp_ms: 13-digit milliseconds. Snap follows Meta's envelope with a different action_source enum.

A shared object named event_time will be wrong on at least one hop. Store one UTC instant. Convert at the edge. The field name is not a hint you can trust across companies. See What is a conversion API and Timestamp units.

// Meta / Pinterest: 10-digit seconds
{"event_time":1770000000}

// Reddit v3 / LinkedIn: 13-digit milliseconds
{"event_at":1770000000000}
{"conversionHappenedAt":1770000000000}

// GA4 MP: 16-digit microseconds
{"timestamp_micros":1770000000000000}

// TikTok Events API: ISO 8601. An epoch here is arrival time.
{"timestamp":"2026-08-21T18:04:00Z"}

Hashing is a split list, not a helper

On Meta, em, ph, fn, ln, ge, db, ct, st, zp, country, and external_id are SHA-256 hex. client_ip_address, client_user_agent, fbp, fbc, fbclid, and event_source_url are not. A generic hash-all-strings helper deletes the strongest identifiers you had and is vendor.meta-conversions-api.body.hashed_plaintext_field on the IP and UA slots. vendor.meta-conversions-api.body.unhashed_email is the inverse on em.

TikTok hashes context.user.email, phone_number, and external_id, and wants context.ip and context.user_agent in the clear. Snap and Pinterest follow the same split with their own field names. Reddit may accept raw email. Do not take that as a Meta policy. Google Ads hashedEmail is SHA-256 after a stricter Gmail normalize (dots and plus-tags). One hasher across Meta and Google Ads will miss one of them on Gmail-heavy lists. See Hashing PII and Do not hash IP or user-agent.

action_source is an enum per vendor

Meta: email, website, app, phone_call, chat, physical_store, system_generated, business_messaging, other. There is no default. Omitting the field is an error. website requires event_source_url. Pinterest is web, app_android, app_ios, or offline. Snap is WEB, OFFLINE, or MOBILE_APP. Reddit v3 is WEBSITE, APP, PHYSICAL_STORE, or OTHER.

Bare JSON has no host. Pixellint's Meta, Snap, and Pinterest packs tell each other apart by action_source. A payload whose action_source is missing or misspelled matches more than one pack, and each reports it. That is why a typo looks like three vendors yelling at once. website is Meta. web is Pinterest. WEB is Snap. WEBSITE is Reddit. See action_source.

CLI, npm, CI

The playground is the same engine as the crate and the npm package. Put the fixtures in CI. Do not paste production CAPI bodies into tickets. Staging orders with staging pixel ids are the fixtures you keep. A weekly screenshot of Test Events is not a contract test.

npm install pixellint, then validate with kind json. cargo install pixellint for the CLI. MCP: cargo install pixellint-mcp. Same rule ids in all three, so a finding in QA is the same ticket in CI. Pin --rulepack so a Meta body does not get Snap findings because action_source 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
pixellint validate json @li.json --rulepack vendor/linkedin-conversions-api

# npm
import { validate, isOk } from "pixellint";
const summary = validate(body, { kind: "json" });
isOk(summary);

What to keep in the fixture folder

A legal Meta Purchase. A legal Meta Lead without fake value. A Meta Purchase that should fail: 13-digit event_time. A Meta Purchase that should fail: raw email. A production fixture that must not contain test_event_code. A staging fixture that must contain it. A TikTok CompletePayment with ISO 8601. A Reddit v3 event with 13-digit event_at and WEBSITE. A LinkedIn conversion with the URN and conversionHappenedAt in milliseconds.

That is the conversion API validator in the pipeline. Test Events stays as a smoke check after publish. The contract does not wait on Ads Manager. When a marketer files CAPI not working, the first reply is the redacted JSON and the linter output, not a token rotation.

pixellint validate json @fixtures/meta-capi-purchase.json --rulepack vendor/meta-conversions-api
pixellint validate json @fixtures/meta-capi-lead.json --rulepack vendor/meta-conversions-api
pixellint validate json @fixtures/tiktok-complete-payment.json --rulepack vendor/tiktok-events-api

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Artifacts you test may be stored; see privacy.