A thank-you page fires the browser Pinterest tag and a server POST to api.pinterest.com/v5/ad_accounts/{id}/events. Ads Manager on Meta looks healthy. Pinterest reporting shows two checkouts for one order.
The usual suspect is a duplicate GTM tag. The usual contract gap is event_id.
{
"data": [
{
"event_name": "checkout",
"action_source": "web",
"event_time": 1770000000,
"user_data": {
"em": "6a7a73766627eb611720883d5a11cc62b5bfee237b00a6658d78c50032ec4aee",
"client_ip_address": "203.0.113.42",
"client_user_agent": "Mozilla/5.0"
},
"custom_data": {
"value": "129.99",
"currency": "USD"
}
}
]
}
That body is structurally close to what many teams already send to Meta's Graph events edge. It is not complete for Pinterest. Pinterest documents event_id on each event so the API can deduplicate against the Pinterest tag on the same conversion. Without it, the server event and the tag event are two rows.
Meta is different on the same field. Meta recommends event_id for deduplication between the pixel and Conversions API. Missing event_id on Meta is a warning, not a hard failure. The POST can still return 200 and the event can still land. Teams that wired dedup only because Meta's Events Manager nagged them often never add the field to the Pinterest branch of a shared template.
What Pinterest actually requires
Pinterest's v5 events payload wraps one or more events in data[]. Each object needs event_name, action_source, event_id, event_time, and user_data with at least one match key (em, hashed_maids, or IP plus user agent).
action_source for web is lowercase web, not Meta's website and not Snap's WEB. Copying Meta's enum verbatim is a silent routing bug before dedup even matters.
event_time is Unix seconds, ten digits, like Meta. Thirteen digits are milliseconds and belong on LinkedIn's clock, not Pinterest's.
Standard event names are lowercase strings such as checkout, add_to_cart, and page_visit. Meta's Purchase string is not Pinterest's purchase event. That naming split is its own failure mode; the short version is that checkout is not Purchase on the wire.
For dedup specifically, mint one stable id per conversion (order id, checkout token, UUID generated once on the thank-you page) and send the same value on the Pinterest tag and the CAPI POST. Retries should reuse the id; new ids on retry look like new conversions.
How you catch it before finance does
I maintain Pixellint, an open source linter for pixels and conversion API payloads. The Pinterest Conversions API pack (vendor/pinterest-conversions-api) contracts data[] against Pinterest's published parameter docs.
On the payload above, missing event_id:
$ pixellint validate json @pinterest-checkout.json
rulepack: vendor/pinterest-conversions-api (vendor: pinterest)
error vendor.pinterest-conversions-api.body.event_id.missing
`event_id` is missing. It identifies the event for deduplication
against the Pinterest tag.
fix: Send a stable id per conversion, such as an order number.
docs: https://developers.pinterest.com/docs/track-conversions/track-conversions-in-the-api/
Run the same file against Meta's pack and you get a warning, exit code 0:
rulepack: vendor/meta-conversions-api (vendor: meta)
warning vendor.meta-conversions-api.body.event_id.missing
That asymmetry is why CI that only fails on Meta errors misses Pinterest. Lint both packs on the same canonical event object, or store one internal event and map fields at the vendor edge.
Paste the body into the browser playground if you do not want a local install. Point the CLI at a saved URL that hits api.pinterest.com when you are testing the full path:
cargo install pixellint
# or: npm install pixellint
pixellint validate json @pinterest-checkout.json --rulepack vendor/pinterest-conversions-api
The field table and rule ids live on the Pinterest Conversions API pack page. For how the img tag and the POST share clocks and names, read the Pinterest tag and CAPI playbook.
Where to go next
If one JSON blob fans out to Meta, Pinterest, Snap, and Reddit from the same Cloud Function, read one CAPI JSON cannot serve every vendor: the adapter belongs at the edge, not in a shared schema everyone "mostly" accepts.
Event time units maps seconds, milliseconds, microseconds, and ISO 8601 across vendors so you are not guessing from one sample payload. Conversions API payloads covers batch shapes, when to fire from the order server, and how dedup ids should survive retries.
The short version
Pinterest requires event_id on every CAPI event for tag dedup. Meta recommends the same field but ingests without it. A shared worker that only enforces Meta's warnings will double-count on Pinterest while both endpoints return success. Send lowercase web, lowercase checkout, seconds on event_time, and the same dedup id on the tag and the POST.
Pixellint is independent of Pinterest and Meta. The rule ids cite their docs because that is where the requirements live, not because this is an official vendor tool.
Top comments (0)