Setting up the Pinterest Conversions API means POSTing conversion events to https://api.pinterest.com/v5/ad_accounts/{ad_account_id}/events with a conversion token as a Bearer credential. The mechanics resemble the Meta Conversions API closely enough that most developers assume they can port their Meta payload and change the URL.
They can’t, and the differences produce a 422 rather than a helpful error. Pinterest accepts exactly nine event names, its hashed user-data fields must be arrays where Meta takes scalars, and custom_data.value must be a string where Meta takes a number.
What do you need before you start?
Three things, and one of them is not an API credential.
A Pinterest ad account ID. It goes in the URL path, not in the payload. You’ll find it in Ads Manager.
A conversion token. This is generated in the Pinterest developer portal and scoped to a specific ad account. It is not an OAuth access token — there’s no authorization flow, no refresh, no app install. You paste a string and send it as Authorization: Bearer <token>. An OAuth access token also works, but the purpose-scoped conversion token is simpler and is what most setups use.
The Pinterest tag still installed on your site. This is the one people skip, and it’s the one that costs the most. Pinterest’s server-side matching leans on click_id — the epik token — more heavily than Meta’s does, because Pinterest’s own identity graph is weaker. The tag is what captures that token. Remove it and your server events still arrive; they just match far fewer people.
What events does Pinterest accept?
Exactly nine, and it rejects anything else with a 422.
| Event name | Typical use |
|---|---|
page_visit |
A page or product view |
view_category |
A collection or category page |
search |
An on-site search |
add_to_cart |
Item added to basket |
checkout |
A completed purchase |
signup |
Account registration |
lead |
A form submission or enquiry |
watch_video |
Video engagement |
custom |
Anything that doesn’t fit the eight above |
Two traps live in this table.
checkout means completed purchase, not checkout started. Pinterest has no begin-checkout event. The instinct is to fire checkout when someone reaches the payment page and again when they pay — which counts every real purchase two or three times and inflates reported conversions. Fire checkout only on a confirmed order.
custom is a literal event name, not a placeholder. If you have an event that isn’t one of the standard eight, the event_name you send is the literal string custom. Your own label for it goes in your own records, not on the wire. Sending your label as the event name gets a 422 for an unknown event, which is the single most common first-attempt failure.
What are the v5 formatting quirks that break implementations?
Three, and every one of them is a silent divergence from the Meta payload people port from.
1. Hashed user data must be arrays. Meta accepts "em": "<hash>". Pinterest’s v5 schema expects "em": ["<hash>"]. This applies to every hashed field — email, phone, first and last name, city, state, zip, country, external ID. One-element arrays are conventional.
The exceptions are the fields that aren’t hashed: click_id, client_ip_address and client_user_agent are plain single strings, exactly as in Meta.
2. custom_data.value must be a string. Send "value": "72.39", not "value": 72.39. This one is especially nasty because a numeric value doesn’t always fail loudly — it can be accepted and mishandled.
3. State must be the abbreviation, hashed. Pinterest’s match index expects the hash of ca, not the hash of california. Normalise to the two-letter code before hashing, or the field matches nothing while looking perfectly well-formed.
A minimal, correctly-shaped payload:
{
"data": [
{
"event_name": "checkout",
"action_source": "web",
"event_time": 1757500000,
"event_id": "a3f1c8...",
"event_source_url": "https://yoursite.com/thank-you",
"user_data": {
"em": ["2a4b6c..."],
"ph": ["9d1e3f..."],
"ct": ["7b2a91..."],
"st": ["4c8de0..."],
"click_id": "dj0yJnU9...",
"client_ip_address": "203.0.113.9",
"client_user_agent": "Mozilla/5.0 ..."
},
"custom_data": {
"currency": "USD",
"value": "72.39",
"order_id": "10482"
}
}
]
}
Pinterest also requires at least one of: a hashed email, or the combination of IP address and user agent. An event carrying neither is rejected rather than accepted-and-unmatched.

How does the epik click ID work?
epik is Pinterest’s click identifier, and it arrives two ways.
On a fresh ad click it’s in the URL as ?epik=.... On a later visit it’s in the _epik cookie, which Pinterest’s tag sets and which persists for roughly 30 days.
The correct resolution order is URL first, cookie second. The URL parameter describes the click that is happening right now, which is authoritative; the cookie is a snapshot of some earlier click. Take the cookie only when the URL has nothing.
Send it as click_id inside user_data, unhashed. It’s an opaque token, not personal data, and hashing it destroys it.
This is worth more attention on Pinterest than on other platforms. Pinterest’s ability to resolve a server event to a user without a click ID is weaker than Meta’s, so the cookie fallback on returning-visitor traffic is a real attribution lift rather than a nicety.
How do you deduplicate against the Pinterest tag?
With a shared event_id on both the browser event and the server event, exactly as on Meta — and with your own dedup as the real defence.
Derive event_id deterministically from something stable about the conversion: an order ID, a lead ID, hashed together with the event name and account. Two things follow. A retried webhook produces the same ID and therefore the same event, so it can’t double-count. And the browser tag and your server, seeing the same conversion, produce the same ID, so Pinterest can collapse them.
The part worth being blunt about: don’t rely on the platform’s deduplication as your only protection. If your automatic path and a manual re-send derive the ID differently, they are two different events as far as Pinterest is concerned, and both will count. A uniqueness constraint on your own side — one row per configuration and event ID — is what actually prevents double-attribution.
How do you know a Pinterest event actually landed?
Read the response body. An HTTP 200 does not mean the event was processed.
Pinterest returns a JSON envelope containing num_events_received, num_events_processed, and a per-event status. Two failure shapes hide behind a 200:
- Envelope rejection —
num_events_receivedcomes back as0. The request was well-formed HTTP and the event inside it was not accepted. - Per-event rejection — the event is received but its
statusis something other thanprocessed.
Neither raises an HTTP error. Monitoring that checks only the status line will report a perfectly healthy integration that is delivering nothing.
Pinterest also doesn’t provide a per-event error code — only top-level error responses carry numeric codes — so the per-event status and message are the whole diagnostic surface. There’s a test mode for this: append ?test=true to the endpoint and the event is validated without polluting real attribution.
Two operational limits worth knowing while you’re building. Events older than seven days are outside the accepted window, so historical backfills need their timestamps clamped rather than sent raw. And the rate limit is 5,000 calls per minute per ad account — generous for most accounts, but relevant if you’re replaying history.

How does PartialLeads send Pinterest conversions?
As one of five server-side conversion integrations, sharing the same identity and purchase pipeline rather than bolting on a separate Pinterest tool.
The epik token is captured at first touch and resolved correctly. The tag reads ?epik= from the URL and snapshots the _epik cookie, and dispatch prefers the URL value over the cookie. For a purchase, the click ID is drawn from the buyer’s session cluster — the first session carrying one wins — so a click that happened weeks before the order still supplies the identifier.
The payload is built to Pinterest’s schema, not Meta’s. Hashed fields go out as one-element arrays, click_id and IP and user agent stay plain, value is serialised as a string, and state is normalised to its two-letter form before hashing. These are handled in one canonical builder per platform rather than per call site.
Event names are constrained to the nine. A configuration labelled as custom sends the literal custom on the wire while keeping your own label in your records, so a descriptive internal name can’t cause a 422.
Pre-purchase steps aren’t mapped to checkout. Page and product views map to page_visit, collections to view_category, on-site search to search, add-to-cart to add_to_cart — and cart views, checkout-started and payment-info events map to nothing at all, deliberately, because Pinterest has no begin-checkout event and mapping them to checkout would multiply every real purchase.
A 200 is not treated as success. The response envelope is parsed; zero events received, or a per-event status other than processed, is recorded as a failure rather than a delivery. Failures are classified — authentication, permanent, rate-limited, transient, configuration — and only the ones that are genuinely final are written to the deduplication ledger, so a transient failure retries instead of being silently swallowed as a completed send.
Deduplication is enforced on our side. Event IDs are derived deterministically and a uniqueness constraint on configuration plus event ID means a redelivered webhook physically cannot double-fire.
A health strip and a test send. Seven-day sends, a per-day sparkline, delivery rate and failed count, with a test-send that always runs in Pinterest’s test mode so it can’t touch real attribution. Generic page-view rows with no product context are excluded from the volume figures, because counting navigation pixels as conversions would flatter the number.
Honest boundaries. Three. Match quality still depends on the _epik cookie being captured client-side — keep the Pinterest tag installed, because a server event without a click ID is an anonymous server event. Events are sent one per request rather than batched, which is fine at normal volume and is a real constraint during a large historical replay. And the seven-day event window is a platform-side limit: older conversions deliver with a clamped timestamp rather than their original one.
| Problem | PartialLeads mechanism | Where you see it working |
|---|---|---|
422 from an unknown event name |
Wire event constrained to Pinterest’s nine, custom label kept off the wire | Accepted events in the activity log |
| Meta-shaped payload rejected | Per-platform payload builder — arrays, string value, abbreviated state | Retained request body per send |
| Purchases counted two or three times | Pre-purchase steps deliberately unmapped; checkout only on a confirmed order |
Purchase count matching your order count |
| Click ID missing on returning visitors | URL epik preferred, _epik cookie fallback, resolved across the session cluster |
Click ID present on cluster-matched purchases |
| HTTP 200 recorded as a delivery | Envelope and per-event status parsed before success is declared | Delivery rate on the health strip |
| Retried webhook double-fires | Deterministic event ID plus a uniqueness constraint | One row per conversion |
| No idea whether it works at all | Health strip plus a test send in Pinterest’s test mode | Sends, sparkline, delivery rate, failures |
Tell us what's broken. We'll fix your tracking — free.
Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.
Sources
- Pinterest Developers — Track conversions in the API: https://developers.pinterest.com/docs/track-conversions/track-conversions-in-the-api/
- Pinterest Developers — API v5 introduction: https://developers.pinterest.com/docs/api/v5/introduction/
- Pinterest Business Help — Pinterest tag: https://help.pinterest.com/en/business/article/track-conversions-with-pinterest-tag
- Wikipedia — SHA-2 hash family: https://en.wikipedia.org/wiki/SHA-2