vendor/meta-conversions-api · vendor documented
Meta CAPI and Facebook CAPI want seconds, not Date.now()
Meta CAPI and Facebook CAPI post a Purchase to the Graph API events edge and return 200. Events Manager stays empty. The usual suspect is the access token. The usual bug is the clock.
event_time is a Unix timestamp in seconds, ten digits, and at most seven days old when it arrives. Date.now() is milliseconds. The event lands far in the future, outside the window, and never attributes. That is the usual conversions API miss: CAPI 200, Test Events empty.
Facebook CAPI is Meta CAPI
Facebook CAPI, Meta CAPI, Meta Conversions API, and Facebook Conversions API are one Graph events POST. conversion API events use event_time in seconds. A CAPI tester lints that CAPI JSON before Test Events.
event_time is seconds, not milliseconds
Send Math.floor(Date.now() / 1000). The same Date.now() is the correct unit on LinkedIn and Amplitude. 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.
em, ph, name, city, and the rest of the customer information parameters are SHA-256 hex digests. client_ip_address and client_user_agent are sent in the clear. Hashing those two makes the event unmatchable.
Rule: vendor.meta-conversions-api.body.hashed_plaintext_field
Purchase needs value and currency
A payload that only names the product is a legal JSON object and an incomplete conversion. Currency is an ISO 4217 code (USD), not a dollar sign.
Rule: vendor.meta-conversions-api.body.purchase_requires_value_and_currency
What this pack matches
Rules
Codes are stable. A finding in CI, MCP, or the playground lands on the same id.
| Field | Required | What it checks | Rule ids | Source |
|---|---|---|---|---|
access_token |
required | The events edge authenticates with an access token on the request. Fix: Send a system user access token, and keep it server-side. | vendor.meta-conversions-api.param.access_token.missingvendor.meta-conversions-api.param.access_token.empty |
docs |
test_event_code |
optional | It routes events to the Events Manager test tool instead of production reporting, so it should not appear on live traffic. Fix: Drop `test_event_code` outside of testing. | vendor.meta-conversions-api.param.test_event_code.empty |
docs |
pii.unhashed_email |
required | A query parameter carries what looks like a raw email address. Conversions API customer information must be normalized and SHA-256 hashed. Fix: Hash the value with SHA-256 and send it in the event payload rather than on the URL. | vendor.meta-conversions-api.pii.unhashed_email |
docs |
testing.test_event_code_present |
recommended | The request carries `test_event_code`, which routes events to the Events Manager test tool instead of production reporting. Fix: Drop `test_event_code` before this runs against live traffic. | vendor.meta-conversions-api.testing.test_event_code_present |
docs |
event_name |
required | It is the standard or custom event name, and it has to match the browser event for deduplication to work. Fix: Set `event_name` to the standard event, such as `Purchase`, or to your custom event name. | vendor.meta-conversions-api.body.event_name.missingvendor.meta-conversions-api.body.event_name.empty |
docs |
event_time |
required | Meta documents it as a Unix timestamp in seconds, and it can be up to 7 days before the event is sent. 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. | vendor.meta-conversions-api.body.event_time.missingvendor.meta-conversions-api.body.event_time.emptyvendor.meta-conversions-api.body.event_time.invalid |
docs |
action_source |
required | It tells Meta where the conversion happened, and Meta accepts a fixed set of values. Fix: Use one of the documented values; website conversions are `website`. | vendor.meta-conversions-api.body.action_source.missingvendor.meta-conversions-api.body.action_source.emptyvendor.meta-conversions-api.body.action_source.invalid |
docs |
user_data |
required | Customer information is required on every server event, and it is what Meta matches the conversion to a person with. Fix: Add a `user_data` object with at least one normalized, hashed identifier. | vendor.meta-conversions-api.body.user_data.missing |
docs |
event_id |
recommended | Meta recommends it for deduplication: the browser pixel and the server event have to carry the same `event_id` and `event_name` to be counted once. Fix: Generate one id per conversion and send the same value from the pixel and the Conversions API. | vendor.meta-conversions-api.body.event_id.missingvendor.meta-conversions-api.body.event_id.empty |
docs |
event_source_url |
optional | It is the page URL where the conversion happened, and Meta requires it for website events. Fix: Send the full URL of the page, including the scheme. | vendor.meta-conversions-api.body.event_source_url.emptyvendor.meta-conversions-api.body.event_source_url.invalid |
docs |
opt_out |
optional | When true, the event is used for attribution only and not for ads delivery optimization. | docs | |
data_processing_options[] |
optional | Meta accepts `LDU` for Limited Data Use, or an empty array. Fix: Send `["LDU"]` to enable Limited Data Use, or `[]` to leave it off. | vendor.meta-conversions-api.body.data_processing_options[].emptyvendor.meta-conversions-api.body.data_processing_options[].invalid |
docs |
data_processing_options_country |
optional | It is 1 for the United States, or 0 to let Meta geolocate. | vendor.meta-conversions-api.body.data_processing_options_country.emptyvendor.meta-conversions-api.body.data_processing_options_country.invalid |
docs |
data_processing_options_state |
optional | It is 1000 for California, or 0 to let Meta geolocate. | vendor.meta-conversions-api.body.data_processing_options_state.emptyvendor.meta-conversions-api.body.data_processing_options_state.invalid |
docs |
custom_data.value |
optional | Meta requires it for purchase events and for any event that uses value optimization. Fix: Send the monetary value as a number, without a currency symbol. | vendor.meta-conversions-api.body.custom_data.value.emptyvendor.meta-conversions-api.body.custom_data.value.invalid |
docs |
custom_data.currency |
optional | Meta requires a valid ISO 4217 three-digit currency code. Fix: Use the three-letter code, such as `USD`. | vendor.meta-conversions-api.body.custom_data.currency.emptyvendor.meta-conversions-api.body.custom_data.currency.invalid |
docs |
custom_data.content_ids[] |
optional | It is a product ID associated with the event, such as a SKU on AddToCart. Fix: Send catalog IDs in `custom_data.content_ids`, or drop empty entries. | vendor.meta-conversions-api.body.custom_data.content_ids[].empty |
docs |
custom_data.content_type |
optional | It says whether `content_ids` and `contents` are products or product groups. Meta documents `product` or `product_group`. Fix: Set `custom_data.content_type` to `product` or `product_group`. | vendor.meta-conversions-api.body.custom_data.content_type.emptyvendor.meta-conversions-api.body.custom_data.content_type.invalid |
docs |
custom_data.contents[].id |
optional | It is the product ID on a contents row. Meta documents `id`, `quantity`, `item_price`, and `delivery_category` on that object. Fix: Set `custom_data.contents[].id` to the catalog item ID, or drop the empty pair. | vendor.meta-conversions-api.body.custom_data.contents[].id.empty |
docs |
custom_data.contents[].quantity |
optional | It is how many of that product the event covers. Fix: Send a whole number in `custom_data.contents[].quantity`, such as `1`. | vendor.meta-conversions-api.body.custom_data.contents[].quantity.emptyvendor.meta-conversions-api.body.custom_data.contents[].quantity.invalid |
docs |
custom_data.contents[].item_price |
optional | It is the price of that product. Put the order total in `custom_data.value`. Fix: Send `custom_data.contents[].item_price` as a number, such as `129.99`, without a currency symbol. | vendor.meta-conversions-api.body.custom_data.contents[].item_price.emptyvendor.meta-conversions-api.body.custom_data.contents[].item_price.invalid |
docs |
custom_data.contents[].delivery_category |
optional | It is the delivery type on a contents row. Meta documents `in_store`, `curbside`, and `home_delivery`. Fix: Set the contents `delivery_category` to `in_store`, `curbside`, or `home_delivery`. | vendor.meta-conversions-api.body.custom_data.contents[].delivery_category.emptyvendor.meta-conversions-api.body.custom_data.contents[].delivery_category.invalid |
docs |
custom_data.delivery_category |
optional | It is the delivery type for a purchase event. Meta documents `in_store`, `curbside`, and `home_delivery`. Fix: Set `custom_data.delivery_category` to `in_store`, `curbside`, or `home_delivery`. | vendor.meta-conversions-api.body.custom_data.delivery_category.emptyvendor.meta-conversions-api.body.custom_data.delivery_category.invalid |
docs |
custom_data.num_items |
optional | It is the number of items in checkout. Meta documents it for InitiateCheckout events. Fix: Send a whole number in `custom_data.num_items`, such as `2`. | vendor.meta-conversions-api.body.custom_data.num_items.emptyvendor.meta-conversions-api.body.custom_data.num_items.invalid |
docs |
custom_data.order_id |
optional | It is the order ID for this transaction, as a string. Fix: Send `custom_data.order_id`, or drop the empty pair. | vendor.meta-conversions-api.body.custom_data.order_id.empty |
docs |
custom_data.search_string |
optional | It is the query the user searched for. Meta documents it for Search events. Fix: Send the query in `custom_data.search_string`, or drop the empty pair. | vendor.meta-conversions-api.body.custom_data.search_string.empty |
docs |
custom_data.predicted_ltv |
optional | It is the predicted lifetime value of a conversion event. Fix: Send `custom_data.predicted_ltv` as a number, such as `250.00`. | vendor.meta-conversions-api.body.custom_data.predicted_ltv.emptyvendor.meta-conversions-api.body.custom_data.predicted_ltv.invalid |
docs |
referrer_url |
optional | It is the referrer URL for the event. Fix: Send an absolute URL in `referrer_url`, including the scheme. | vendor.meta-conversions-api.body.referrer_url.emptyvendor.meta-conversions-api.body.referrer_url.invalid |
docs |
user_data.em |
optional | Email must be trimmed, lowercased, and SHA-256 hashed before it is sent. Fix: Normalize the address, hash it with SHA-256, and send the hex digest. | vendor.meta-conversions-api.body.user_data.em.emptyvendor.meta-conversions-api.body.user_data.em.invalid |
docs |
user_data.em[] |
optional | Every email in the list must be normalized and SHA-256 hashed. Fix: Normalize each address, hash it with SHA-256, and send the hex digests. | vendor.meta-conversions-api.body.user_data.em[].emptyvendor.meta-conversions-api.body.user_data.em[].invalid |
docs |
user_data.ph |
optional | Phone numbers must have symbols and letters removed, carry the country code, and be SHA-256 hashed. Fix: Normalize the number to digits with the country code, hash it with SHA-256, and send the hex digest. | vendor.meta-conversions-api.body.user_data.ph.emptyvendor.meta-conversions-api.body.user_data.ph.invalid |
docs |
user_data.ph[] |
optional | Every phone number in the list must be normalized and SHA-256 hashed. Fix: Normalize each number to digits with the country code and hash it with SHA-256. | vendor.meta-conversions-api.body.user_data.ph[].emptyvendor.meta-conversions-api.body.user_data.ph[].invalid |
docs |
user_data.fn |
optional | First name must be lowercased and SHA-256 hashed. | vendor.meta-conversions-api.body.user_data.fn.emptyvendor.meta-conversions-api.body.user_data.fn.invalid |
docs |
user_data.ln |
optional | Last name must be lowercased and SHA-256 hashed. | vendor.meta-conversions-api.body.user_data.ln.emptyvendor.meta-conversions-api.body.user_data.ln.invalid |
docs |
user_data.ge |
optional | Gender is sent as a lowercase initial and SHA-256 hashed. | vendor.meta-conversions-api.body.user_data.ge.emptyvendor.meta-conversions-api.body.user_data.ge.invalid |
docs |
user_data.db |
optional | Date of birth is normalized to YYYYMMDD and SHA-256 hashed. | vendor.meta-conversions-api.body.user_data.db.emptyvendor.meta-conversions-api.body.user_data.db.invalid |
docs |
user_data.ct |
optional | City is lowercased with punctuation and spaces removed, then SHA-256 hashed. | vendor.meta-conversions-api.body.user_data.ct.emptyvendor.meta-conversions-api.body.user_data.ct.invalid |
docs |
user_data.st |
optional | State is the lowercase two-character code, SHA-256 hashed. | vendor.meta-conversions-api.body.user_data.st.emptyvendor.meta-conversions-api.body.user_data.st.invalid |
docs |
user_data.zp |
optional | Zip is lowercased without spaces or dashes, then SHA-256 hashed. | vendor.meta-conversions-api.body.user_data.zp.emptyvendor.meta-conversions-api.body.user_data.zp.invalid |
docs |
user_data.country |
optional | Country is the lowercase ISO 3166-1 alpha-2 code, SHA-256 hashed. Fix: Hash the two-letter code rather than sending it in the clear. | vendor.meta-conversions-api.body.user_data.country.emptyvendor.meta-conversions-api.body.user_data.country.invalid |
docs |
user_data.external_id |
optional | Meta recommends hashing the advertiser's own identifier with SHA-256. | vendor.meta-conversions-api.body.user_data.external_id.emptyvendor.meta-conversions-api.body.user_data.external_id.invalid |
docs |
user_data.client_ip_address |
optional | It is sent unhashed, and it must be the real IPv4 or IPv6 address of the browser. Fix: Send the client address as-is; do not hash it and do not send your server's address. | vendor.meta-conversions-api.body.user_data.client_ip_address.empty |
docs |
user_data.client_user_agent |
optional | It is sent unhashed, and it must be the browser's user agent rather than your HTTP client's. Fix: Forward the browser `User-Agent` header from the original request. | vendor.meta-conversions-api.body.user_data.client_user_agent.empty |
docs |
user_data.fbc |
optional | Meta documents the click id cookie as `fb.${subdomain_index}.${creation_time}.${fbclid}`. Fix: Send the `_fbc` cookie verbatim, or rebuild it in the documented format. | vendor.meta-conversions-api.body.user_data.fbc.emptyvendor.meta-conversions-api.body.user_data.fbc.invalid |
docs |
user_data.fbp |
optional | Meta documents the browser id cookie as `fb.${subdomain_index}.${creation_time}.${random_number}`. Fix: Send the `_fbp` cookie verbatim. | vendor.meta-conversions-api.body.user_data.fbp.emptyvendor.meta-conversions-api.body.user_data.fbp.invalid |
docs |
user_data.lead_id |
optional | It is the numeric id Meta generated for a Lead Ads lead. | vendor.meta-conversions-api.body.user_data.lead_id.emptyvendor.meta-conversions-api.body.user_data.lead_id.invalid |
docs |
body.purchase_requires_value_and_currency |
required | A `Purchase` event is missing `custom_data.value` or `custom_data.currency`. Meta documents both as required for purchase events. Fix: Add `custom_data.value` as a number and `custom_data.currency` as an ISO 4217 code. | vendor.meta-conversions-api.body.purchase_requires_value_and_currency |
docs |
body.website_requires_source_url |
required | The event is marked `action_source: website` but carries no `event_source_url`, which Meta requires for website events. Fix: Send the page URL the conversion happened on. | vendor.meta-conversions-api.body.website_requires_source_url |
docs |
body.ldu_requires_country |
required | Limited Data Use is enabled but `data_processing_options_country` is missing. Fix: Send `data_processing_options_country` as 1 for the United States, or 0 to let Meta geolocate. | vendor.meta-conversions-api.body.ldu_requires_country |
docs |
body.unhashed_email |
required | A field carries what looks like a raw email address. Meta requires customer information to be normalized and SHA-256 hashed before it is sent. Fix: Trim the address, lowercase it, hash it with SHA-256, and send the hex digest. | vendor.meta-conversions-api.body.unhashed_email |
docs |
body.hashed_plaintext_field |
required | 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. | vendor.meta-conversions-api.body.hashed_plaintext_field |
docs |
Validate a payload
pixellint validate json @payload.json --rulepack vendor/meta-conversions-api
Try this failing payload in the playground. event_time in milliseconds, raw email.
{"data":[{"event_name":"Purchase","event_time":1770000000000,"action_source":"website","user_data":{"em":"buyer@example.com"}}]}
cargo install pixellint
·
npm install pixellint