DEV Community

Aleksander Sekowski
Aleksander Sekowski

Posted on

Meta's Conversions API Wants Seconds. Date.now() Sends Milliseconds.

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  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.
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
  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.
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode
  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.
Enter fullscreen mode Exit fullscreen mode

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).
Enter fullscreen mode Exit fullscreen mode

Exit code 1 when any error-severity finding is present. Warnings do not fail the run.

cargo install pixellint
# or: npm install pixellint
Enter fullscreen mode Exit fullscreen mode

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)