A checkout handler posts a Purchase to the Meta Conversions API. The request returns 200. Events Manager stays empty.
The usual suspect is the access token. The usual bug is the clock.
event_time: Date.now()
Date.now() is milliseconds since epoch. Thirteen digits. Meta documents event_time as a Unix timestamp in seconds, ten digits, and at most seven days old when it arrives. A 13-digit value is a timestamp tens of thousands of years in the future. It is outside the window. It does not attribute. The HTTP status does not mention any of that.
The fix is one line:
event_time: Math.floor(Date.now() / 1000)
I maintain Pixellint, an open source linter for pixels and conversion API payloads. The Conversions API rulepack (vendor/meta-conversions-api) contracts the Graph API events edge against Meta's own parameter docs. This is the finding it prints on a millisecond timestamp:
$ pixellint validate json @purchase.json
rulepack: vendor/meta-conversions-api (vendor: meta)
error vendor.meta-conversions-api.body.event_time.invalid
`event_time` must be at most 10 digits, but `1770000000000` has 13.
Meta documents it as a Unix timestamp in seconds... A 13-digit
value is milliseconds and lands far in the future.
fix: Send seconds, not milliseconds: divide a JavaScript `Date.now()`
by 1000 and floor it.
docs: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/server-event
The same Date.now() is the correct unit on LinkedIn (conversionHappenedAt is 13 digits) and Amplitude (time is milliseconds; seconds land in 1970). Copying a payload between vendors without converting the clock is a silent miss on one of them.
Hash the email. Do not hash the IP.
Meta's customer information parameters are not "hash everything." Email, phone, name, city, state, zip, country, gender, date of birth, and external_id are SHA-256 hex digests. client_ip_address and client_user_agent are sent in the clear. Hashing those two makes the event unmatchable.
A raw address in user_data.em fails twice: the field is not a 64-character hex digest, and a separate rule flags the @ as unhashed PII.
"user_data": {
"em": ["shopper@example.com"],
"client_ip_address": "203.0.113.42"
}
error vendor.meta-conversions-api.body.user_data.em[].invalid
`user_data.em[]` is `shopper@example.com`, which does not match
`^[A-Fa-f0-9]{64}$`. Every email in the list must be normalized
and SHA-256 hashed.
error vendor.meta-conversions-api.body.unhashed_email
A field carries what looks like a raw email address.
Normalize first (trim, lowercase), then hash, then send the hex. Meta documents lowercasing the input, not the digest, so an uppercase hex digest is still a valid hash.
The other direction is the "hash everything" helper that ran over the whole user_data object:
"client_ip_address": "17af1cf3d1b5332c53349fc789abdc853bbeea7ed33eff727ff794ab741ccac9"
error vendor.meta-conversions-api.body.hashed_plaintext_field
This field looks like a SHA-256 digest, but Meta documents it as
one of the parameters that is sent unhashed.
fix: Send the raw value. Hashing it makes the event unmatchable.
Purchase without value is not a purchase
Purchase requires custom_data.value and custom_data.currency. A payload that only names the product is a legal JSON object and an incomplete conversion.
"custom_data": { "content_name": "Blue widget" }
error vendor.meta-conversions-api.body.purchase_requires_value_and_currency
A `Purchase` event is missing `custom_data.value` or
`custom_data.currency`. Meta documents both as required for
purchase events.
Currency is an ISO 4217 code (USD), not $.
Website events need a page URL. Dedup needs an id.
action_source: website without event_source_url is an error. Meta requires the page URL for website events.
event_id is a warning rather than an error. Meta recommends it so the browser pixel and the server event count once. Generate one id per conversion and send it on both sides. Without it you either double-count or you throw away the server event in Events Manager's dedup view and cannot tell which.
test_event_code on a live request is also a warning. It routes the event into the test tool instead of production reporting. Easy to leave in after a staging check.
What the CLI actually prints
The pack runs on a URL hitting graph.facebook.com/.../events and on a bare JSON body, which is what you have when the payload is still in a test file.
$ pixellint validate json @purchase.json
rulepack: core
ok
rulepack: vendor/meta-conversions-api (vendor: meta)
error vendor.meta-conversions-api.body.event_time.invalid ...
error vendor.meta-conversions-api.body.purchase_requires_value_and_currency ...
2 error(s), 0 warning(s), 0 info message(s) across 2 rulepack(s).
Exit code 1 when any error-severity finding is present. Warnings do not fail the run.
cargo install pixellint
# or: npm install pixellint
The contracts, with citations, live on the Meta Conversions API pack page. Paste a payload into the browser playground if you do not want a clone. Nothing you paste leaves the browser.
The short version
event_time is seconds, not Date.now(). Hash em and ph. Do not hash client_ip_address or client_user_agent. A Purchase needs value and currency. A website event needs event_source_url. Send the same event_id the pixel sent.
Pixellint is independent of Meta. The rule ids above cite Meta's Conversions API docs because that is where the requirements live, not because this is an official tool.
Top comments (0)