Pinterest CAPI (Server-Side)
The Pinterest Conversions API (CAPI) sends events from your server directly to Pinterest Ads, sidestepping the browser restrictions that increasingly blind the client-side Pinterest Tag. For any advertiser running meaningful Pinterest spend — especially in shopping categories where Safari and mobile web traffic is significant — CAPI is the difference between attribution you can trust and attribution that drifts downward every quarter.
Valid as of April 2026, Pinterest Conversions API v5.
Why server-side for Pinterest
Section titled “Why server-side for Pinterest”Pinterest’s audience skews heavily toward iOS and mobile web — exactly the cohort most affected by Safari’s Intelligent Tracking Prevention and by content/ad blockers. The client-side Pinterest Tag is missing events for a meaningful share of converters long before you notice it in reporting.
The three concrete problems CAPI solves:
- ITP caps client-side cookies at 7 days on Safari, shortening Pinterest’s attribution window for a large slice of its audience.
- Shopping-category ad blockers (uBlock, Brave) routinely block
s.pinimg.compixel loads. - Pinterest’s click identifier (
epik) is sometimes stripped by corporate proxies, so click-side attribution quietly fails without CAPI as a fallback.
When it is not worth it: If Pinterest is less than ~5% of your paid spend and your sGTM container doesn’t already exist for another platform, the marginal attribution lift probably doesn’t justify the setup effort. Run the client-side tag alone and revisit when spend grows.
The event model
Section titled “The event model”Pinterest CAPI v5 accepts a batch of events at a per-ad-account endpoint. Every event needs a name, an action_source, a timestamp, and at least one user match key.
Required per event:
| Field | Notes |
|---|---|
event_name | One of Pinterest’s standard names (see below) or custom |
action_source | web, app_android, app_ios, or offline |
event_time | Unix epoch seconds |
user_data | At least one hashed identifier OR IP + UA |
Standard event names: page_visit, view_category, search, add_to_cart, checkout, signup, lead, watch_video, custom.
Match keys (all SHA-256 hashed, lowercase trimmed, UTF-8):
| Field | Source | Hashed? |
|---|---|---|
em | Yes | |
ph | Phone (E.164 first) | Yes |
external_id | Your internal user ID | Yes |
hashed_maids | Mobile advertising IDs | Yes |
client_ip_address | Request header | No |
client_user_agent | Request header | No |
click_id | epik URL param or _epik cookie | No |
The click_id is Pinterest’s equivalent of Meta’s fbc — it travels from an ad click through the URL and into the landing page cookie. Preserving it through to the server-side event is the single biggest quality lever.
Setup in sGTM
Section titled “Setup in sGTM”-
Generate a Conversion Access Token: Pinterest Business → Conversions → API access → Create token. Store it in an sGTM User-Defined Variable of type “Constant.” Do not hardcode it in tag templates.
-
Get your Ad Account ID from the Pinterest Business → Business manager URL (the numeric ID after
/accounts/). Store it as a second constant variable. -
In your sGTM container, go to Tags → New → Community Template Gallery. Search “Pinterest Conversions API” and install Pinterest’s official template. If your region does not have the template, use a Custom Template with the HTTP shape shown below.
-
Configure the tag:
- Ad Account ID: your stored variable
- Conversion Access Token: your stored variable
- Event Name: mapped from GA4 event name (see mapping table)
- Event ID: mapped from the
event_idevent parameter
-
Add user data variables: Cookie Variable for
_epik, Request Header Variable forX-Forwarded-Foranduser-agent, and Event Data variables for any hashedem/ph/external_idpushed into the GA4 event. -
Set the firing trigger on the relevant GA4 events (
purchase,add_to_cart,view_item,generate_lead,sign_up).
Raw HTTP shape (for custom templates or debugging):
POST https://api.pinterest.com/v5/ad_accounts/<ad_account_id>/eventsAuthorization: Bearer <conversion_access_token>Content-Type: application/json{ "data": [{ "event_name": "checkout", "action_source": "web", "event_time": 1713571200, "event_id": "evt-ORDER-12345-1713571200", "event_source_url": "https://example.com/thank-you", "user_data": { "em": ["<sha256 of email>"], "ph": ["<sha256 of phone>"], "external_id": ["<sha256 of user id>"], "client_ip_address": "203.0.113.45", "client_user_agent": "Mozilla/5.0 ...", "click_id": "dj0yJnU9..." }, "custom_data": { "currency": "USD", "value": "79.99", "order_id": "ORDER-12345", "content_ids": ["SKU-A", "SKU-B"], "num_items": 2 } }]}Event mapping from the Pinterest Tag
Section titled “Event mapping from the Pinterest Tag”Pinterest’s server-side names mostly match the client-side tag, with a few quirks: checkout (not purchase) is the purchase event, and there is no explicit BeginCheckout.
| GA4 event | Pinterest CAPI event | Notes |
|---|---|---|
page_view | page_visit | Optional server-side if client covers it |
view_item | page_visit with content_ids | Or custom if you want a distinct event |
view_item_list | view_category | Pass category name in custom_data |
search | search | Pass search_string |
add_to_cart | add_to_cart | |
begin_checkout | custom (name: BeginCheckout) | Pinterest has no native BeginCheckout |
purchase | checkout | The critical one — value, order_id, content_ids |
generate_lead | lead | |
sign_up | signup |
Parameter mapping for checkout (purchase):
// sGTM event data variables mapped into the Pinterest CAPI tag{ event_name: "checkout", event_id: eventData.event_id, // from dataLayer event_time: Math.floor(eventData.timestamp_ms / 1000), event_source_url: eventData.page_location, custom_data: { currency: eventData.ecommerce.currency, value: String(eventData.ecommerce.value), // Pinterest prefers stringified decimals order_id: eventData.ecommerce.transaction_id, content_ids: eventData.ecommerce.items.map(i => i.item_id), num_items: eventData.ecommerce.items.length }}Deduplication with the Pinterest Tag
Section titled “Deduplication with the Pinterest Tag”Pinterest deduplicates by event_id plus event_name. If both the client-side Pinterest Tag and the CAPI event carry the same event_id for the same event name, Pinterest counts them once.
The dedup window is approximately 7 days — generous compared to Meta’s 48 hours, but do not rely on the slack: generate fresh event_id values per event firing.
Propagation flow:
- Your frontend generates a stable event ID and pushes it with the purchase event:
dataLayer.push({ ecommerce: null });dataLayer.push({ event: 'purchase', event_id: 'pin-' + order.id + '-' + Date.now(), ecommerce: { transaction_id: order.id, currency: 'USD', value: 79.99, items: [...] }});- Client-side GTM maps
{{DL - event_id}}to the Pinterest Tag’sevent_idparameter. - The GA4 tag sends the same
event_idto sGTM as an event parameter. - The sGTM Pinterest CAPI tag reads
event_idfrom the Event Model and sends it on.
Testing and validation
Section titled “Testing and validation”Pinterest Ads Manager → Conversions → Tag Health shows inbound events broken down by source (Tag, CAPI, both). After you deploy:
- Fire a test purchase on staging with real
event_idpropagation. - Wait ~5 minutes — CAPI events typically appear within 2–5 minutes; tag events within 1 minute.
- In Tag Health, confirm the event shows under both “Pinterest Tag” and “Conversions API.”
- Check the “Match Quality” score Pinterest displays — below 5 means you are not sending enough match keys.
Pinterest does not offer a “test event code” mechanism like Meta, so test events go into your real data. Use a staging ad account if you have one, or at minimum filter by timestamp in Tag Health during test windows.
Common mistakes
Section titled “Common mistakes”Sending page_visit from both Tag and CAPI without dedup
Section titled “Sending page_visit from both Tag and CAPI without dedup”The Pinterest Tag fires page_visit on every page load automatically. If your sGTM also sends page_visit for every GA4 page_view event with a different event_id, you double-count page views. Either skip page_visit server-side entirely, or propagate the same event_id for both.
Forgetting to normalize phone numbers before hashing
Section titled “Forgetting to normalize phone numbers before hashing”Pinterest expects phone in E.164 (+15551234567) normalized to lowercase — ph: sha256("+15551234567"). Hashing (555) 123-4567 gives a different hash, and the match fails silently. Normalize on the server before hashing.
Using Pinterest Tag ID instead of Ad Account ID
Section titled “Using Pinterest Tag ID instead of Ad Account ID”The endpoint path takes the Ad Account ID (from Business Manager), not the Pinterest Tag ID (which is a separate numeric value used in the client-side pixel). Mixing them up gives a 404 on every request.
Sending value without currency
Section titled “Sending value without currency”value without currency is treated as unknown and silently excluded from ROAS reporting. Always send both together.