TikTok Events API Setup 2026: Deduplication, Match Keys, and the Attribution Windows Nobody Sets
TikTok delivery learns from what the Events API sends, scores it as Event Match Quality, and attributes it through click, engaged-view, and view windows most accounts never chose. The pixel plus Events API stack, the payload that lifts match quality, deduplication that actually works, the windows to set per ad group, and what changed in 2025 and 2026.
tiktok accounts tend to have a pixel from 2022, an events api integration a shopify app installed, and attribution windows left at whatever the ad group defaulted to. then the reported conversions disagree with the store by 40% and nobody knows which number to believe. this guide is the setup that makes the numbers mean something: both sources, deduplicated, with the match keys tiktok scores, and windows you chose on purpose.
what the tiktok signal stack actually is
two paths into one events dataset, the same pattern as meta.
| path | mechanism | what it carries | what it misses |
|---|---|---|---|
| browser | tiktok pixel (events.js) | real-time events, the _ttp cookie, ttclid from the landing url, page context, advanced matching fields | anything blocked by ad blockers, safari, consent banners; server-only events |
| server | events api (https post) | the same events plus server truth from the payment webhook or crm | browser context unless you forward ip, user agent, ttp, and ttclid |
tiktok's guidance: "we recommend setting up events api as a second channel together with your tiktok pixel" (tiktok ads manager, getting started with events api), with event deduplication so nothing counts twice. tiktok's matching documentation reports that advertisers using both pixel and events api see 19% more events captured and a 15% improvement in cost per action (tiktok ads manager, how to set up matching events with events api).
here is what most guides get wrong: they stop at "install both." tiktok scores each event source on event match quality, "calculated using a weighted average of all your match key coverages" (tiktok ads manager, diagnostic and monitoring tools in events manager), and its delivery models train on the matched events. installing both sources with only an email gets you a score, not a signal.
the fundamental shift: from installing a pixel to feeding a scored signal
before: the pixel fired, events showed up in events manager, the campaign optimized for "complete payment," and attribution was 7-day click by default and nobody looked.
after:
- match quality is scored per event. event match quality runs 0 to 10 and is driven by the coverage of each match key: click id, hashed email, hashed phone, external id, ip address, and user agent. ip and user agent "are shared by default for pixel and require manual configuration for events api." the server integration has to send them.
- attribution is a setting you make per ad group. click-through attribution is "1 day or 7 day," engaged view-through is "1 day or 7 day," and view-through is "off or 1 day," chosen "while creating new ad groups" (tiktok ads manager, about the attribution window). engaged view-through "assigns credit to an ad when a user watches at least 6 seconds of the ad or the full duration for videos shorter than 6 seconds before converting," for app promotion, lead generation, and sales objectives (tiktok ads manager, about engaged view-through attribution). the setting "controls your tiktok ads manager conversion reporting" and "doesn't impact your mmp reporting," so tiktok tells you to keep them consistent.
- smart+ automates the buying. tiktok's automation portfolio, including auto-selected creatives and smart+ web campaigns, optimizes against the events you send. a broken events api starves the automation.
in plain english: send both sources, send every match key from the server, deduplicate on one id, and set the windows per ad group with the same values everywhere you report.
what this broke in existing accounts
1. events api integrations that send email and nothing else
a shopify app or a tag manager template that forwards a hashed email without ip, user agent, ttp, or ttclid. the pixel carries those by default; the server path does not unless configured. the server events score low, and the campaign optimizes on the browser events that safari and ad blockers already thinned.
2. ttclid lost between the landing page and checkout
the click id arrives on the landing url. a checkout on another subdomain, a redirect, or a consent reload drops it. apple's link tracking protection on mail, messages, and private browsing removes known click identifiers before the page even loads. accounts that never persisted ttclid into a first-party cookie attribute paid traffic on ip and user agent.
3. duplicate purchases from two integrations
the shopify channel sends purchase. a second capi app sends purchase. neither shares an event_id. every order counts twice, roas doubles, and the optimization model learns from a doubled signal.
4. attribution windows that vary by ad group
one ad group on 7-day click plus 1-day view, another on 1-day click, a third with engaged view on. campaign comparisons inside the account were comparing different definitions. the mmp used a fourth.
5. reported conversions that disagree with the store by design
7-day click plus engaged view plus 1-day view will always exceed last-click store attribution. teams that treated the gap as a bug rebuilt integrations that were working.
6. optimization events the server never delivered
campaigns set to optimize for complete payment while the events api had delivered add-to-cart only, because the payment webhook was never wired. the ad group learned from the browser event and lost every purchase safari blocked.
7. value in cents
stripe amounts sent raw. 2999 instead of 29.99. reported revenue 100x, value bidding trained on it.
the data
- tiktok: advertisers using both pixel and events api see "19% more events captured" and a "15% improvement in cost per action." self-reported. (tiktok ads manager, matching events with events api)
- tiktok: event match quality is "a weighted average of all your match key coverages"; match keys are click id, email, phone, external id, ip address, and user agent; ip and user agent are automatic for the pixel and manual for the events api. (tiktok ads manager, events manager diagnostics)
- tiktok: attribution windows per ad group are click 1 or 7 days, engaged view 1 or 7 days, view off or 1 day; the setting affects ads manager reporting, not mmp reporting. page last updated february 2025. (tiktok ads manager, attribution window)
- tiktok: engaged view-through credits an ad after at least 6 seconds of watch time, or the full video if shorter, on app promotion, lead generation, and sales objectives. (tiktok ads manager, engaged view-through attribution)
- tiktok, advertising week new york, october 2025: attribution analytics expanded with assisted conversions, "identifying which channel people convert on when tiktok influences and assists the conversion, but tiktok is not the last-click channel," and third-party optimization starting with google analytics (tiktok for business, introducing tiktok's attribution portfolio).
- tiktok's reporting api exposes per-advertiser modeled attribution metrics, including engaged-view conversions, whose valid names vary by advertiser. orbit discovers the valid set per advertiser and caches it, so reports show the same modeled numbers the ads manager dashboard does.
what changed in 2026 specifically
events api 2.0. tiktok's getting-started guidance now references events api 2.0 with access-token authentication and three integration routes: commerce partners (shopify, woocommerce), data partners (cdps, tag managers, crms), and direct api. the direct endpoint remains /open_api/v1.3/event/track/ on the business api.
attribution analytics matured. assisted conversions and third-party optimization launched in late 2025 and expanded through 2026, giving tiktok a first-party view of conversions it influenced but did not close. the numbers are tiktok's; read them next to a geo holdout, not instead of one (see incrementality testing).
smart+ everywhere. auto-selected creatives, smart+ web campaigns, spark ads inside smart+, and new url macros for smart+ ad ids (see the utm standard). automation raises the cost of a weak events api because more of the account runs on it.
apple traffic. safari 26 (september 2025) strips known click identifiers on mail, messages, and private browsing. ttclid on apple traffic is a bonus, not a foundation; hashed identifiers from the server are the durable match keys.
unchanged: sha-256 hashing, event_id deduplication, standard event names, and the 7-day learning window that needs about 50 optimization events.
what actually works now
1. credentials
events manager, web events, select the pixel, copy the pixel id from settings. on the same page, generate an events api access token and store it in a secret manager. the pixel id is the event_source_id on every server call.
2. standard events, spelled exactly
tiktok optimizes against named standard events; a misspelled or custom event is accepted and under-optimized. purchase is not Purchase.
| stage | event | parameters |
|---|---|---|
| page | PageView | none |
| product | ViewContent | content_id, content_type, value, currency |
| cart | AddToCart | content_id, value, currency |
| checkout | InitiateCheckout | value, currency |
| payment info | AddPaymentInfo | value, currency |
| purchase | CompletePayment | value, currency, contents |
| signup | CompleteRegistration | external_id |
| subscription | Subscribe | value, currency |
CompletePayment is the purchase event for web on tiktok; Purchase exists for some partner integrations. check which one your account's campaigns optimize for and send that one from both sources.
3. the match keys, all of them, from the server
| key | field | hash | note |
|---|---|---|---|
email | sha-256 | trim, lowercase first | |
phone | phone | sha-256 | e.164 with country code, then hash |
ttclid | click id | no | from the landing url; persist 90 days in a first-party cookie |
ttp | _ttp cookie | no | set by the pixel; forward it |
external_id | your customer or order id | sha-256 | stable per user |
ip | client ip | no | raw; manual on the server |
user_agent | user agent | no | raw; manual on the server |
the pixel sends ip, user agent, and ttp automatically. the server sends nothing you do not put in the payload.
4. deduplication on one event_id
generate the id once, server-side at render or at checkout start, and use it in both calls. tiktok collapses a pixel event and a server event with the same event name and event_id.
// server generates at checkout start and hands it to the browser
const eventId = crypto.randomUUID();
// browser
ttq.track('CompletePayment', {
value: 29.99,
currency: 'USD',
contents: [{ content_id: 'SKU-123', quantity: 1, price: 29.99 }]
}, { event_id: eventId });
// server, later, from the payment webhook, with the same eventId
5. capture and forward the browser context
// landing page: persist ttclid for 90 days
const params = new URLSearchParams(window.location.search);
const ttclid = params.get('ttclid');
if (ttclid) {
document.cookie = `_orbit_ttclid=${encodeURIComponent(ttclid)}; max-age=${60 * 60 * 24 * 90}; path=/; SameSite=Lax; Secure`;
}
// checkout start: send cookies and the event id to the server
const ttp = document.cookie.match(/_ttp=([^;]+)/)?.[1];
const storedTtclid = document.cookie.match(/_orbit_ttclid=([^;]+)/)?.[1];
await fetch('/api/checkout/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cartId, eventId: crypto.randomUUID(), ttp, ttclid: storedTtclid })
});
if checkout lives on another domain, pass ttclid on the link and re-capture it there. the server stores ttp, ttclid, ip, user agent, and the event id with the order so the webhook has them.
6. the purchase from the payment webhook
// node, events api on the business api
const crypto = require('crypto');
const sha256 = (v) => crypto.createHash('sha256').update(String(v).trim().toLowerCase()).digest('hex');
const digits = (p) => String(p).replace(/\D/g, ''); // caller supplies country code
async function sendTikTokEvent({ eventName, eventId, eventTime, url, user, value, currency, contents, testEventCode }) {
const body = {
event_source: 'web',
event_source_id: process.env.TIKTOK_PIXEL_ID,
...(testEventCode ? { test_event_code: testEventCode } : {}),
data: [{
event: eventName, // 'CompletePayment'
event_time: eventTime, // unix seconds
event_id: eventId, // same as the pixel call
user: {
email: user.email ? sha256(user.email) : undefined,
phone: user.phone ? sha256(digits(user.phone)) : undefined,
external_id: user.id ? sha256(user.id) : undefined,
ttclid: user.ttclid, // raw
ttp: user.ttp, // raw
ip: user.ip, // raw
user_agent: user.userAgent // raw
},
properties: { value, currency: currency || 'USD', contents },
page: { url }
}]
};
const res = await fetch('https://business-api.tiktok.com/open_api/v1.3/event/track/', {
method: 'POST',
headers: { 'Access-Token': process.env.TIKTOK_ACCESS_TOKEN, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return res.json();
}
value. strip test_event_code in production or the events never reach reporting.7. the other integration paths
tag manager, server-side. tiktok pixel template in the web container on all pages; tiktok events api tag in the server container with the access token and pixel id; map event_id, value, currency, hashed email and phone, ttclid, ttp, and let the server container forward ip and user agent.
shopify. the tiktok channel installs pixel and events api and deduplicates on the order. confirm in test events that server purchases carry ttclid and ttp; checkout extensibility does not always forward them. if a second capi app also sends purchase, one of them has to stop.
cdp (segment and similar). the tiktok conversions destination sends server-side. the pixel base code still has to be installed separately, and the same event_id has to reach both.
8. set the attribution windows on purpose
decide one setting for the account and apply it to every ad group at creation: click 7 days, engaged view 7 days, view 1 day for most ecommerce; click 7, engaged view 1, view off for accounts that want a stricter read. write it down, set the same windows in the mmp or attribution tool, and compare campaigns only on matching windows. when reconciling with the store, expect tiktok's number to be higher; the gap is the definition, not a bug. read tiktok's assisted conversions as a directional signal and confirm with a geo holdout.
9. verify weekly
- pixel helper: base code on every page, events at the right steps,
event_idpresent, no duplicate events per load. - test events: run a purchase, confirm both sources with the same
event_idand populated user information. - diagnostics: unnamed events, low match rates, missing deduplication keys, value and currency errors.
- event match quality per event: read the score and which match keys are missing on what share of server events.
| symptom | cause | fix |
|---|---|---|
| events show as unnamed | wrong casing | exact standard names on both sources |
| low match quality on server events | only email sent | add phone, ttclid, ttp, ip, user agent, external_id |
| purchases about 2x orders | no shared event_id, or two integrations | one id in both calls; one owner per event |
| value 100x too high | cents sent raw | divide by 100 |
| no server events | webhook not firing | check the payment webhook |
| campaign comparisons disagree | ad groups on different windows | one window setting account-wide |
what to stop doing immediately
- stop sending email-only server events. the server sends nothing you do not add.
- stop letting
ttcliddie at the landing page. - stop running two integrations that both send the purchase event.
- stop leaving attribution windows at whatever each ad group defaulted to.
- stop treating the tiktok-versus-store gap as an integration bug before checking the windows.
- stop optimizing for complete payment when the events api has never delivered it.
- stop sending stripe cents as value.
the orbit agent instructions
{
"guide": "tiktok-attribution-setup",
"agent_tasks": {
"audit_pixel_events_api_redundancy": {
"check": "for complete payment and the account's optimization event, read the share of events arriving from the pixel, from the events api, and from both over the last 7 days",
"flag": "pixel-only tracking, or fewer than 80 percent of purchase events arriving from both sources",
"recommend": "send the optimization event from the server on the payment webhook and keep the pixel firing for the browser context"
},
"audit_event_deduplication": {
"check": "compare 7-day complete payment counts in events manager with orders in the store or payment processor and read the share of events carrying a shared event_id",
"flag": "purchase count more than 15 percent above orders after accounting for the attribution window, or event_id present on fewer than 90 percent of events from either source",
"recommend": "generate one event_id server-side at checkout start, use it in both the pixel call and the events api payload, and disable any second integration sending purchase"
},
"audit_match_keys": {
"check": "read event match quality per event and the share of server events carrying ttclid, ttp, hashed email, hashed phone, external_id, ip, and user agent",
"flag": "event match quality below 7 on the optimization event, ttclid on fewer than 50 percent of events from paid traffic, ttp below 80 percent, or ip and user agent absent on server events",
"recommend": "persist ttclid from the landing url into a first-party cookie, forward ttp and ttclid at checkout, and add ip, user agent, and hashed email and phone to the server payload"
},
"audit_attribution_windows": {
"check": "read the click, engaged view, and view attribution windows on every active ad group and the windows configured in any mmp or attribution tool",
"flag": "more than one window configuration across active ad groups, or ads manager windows that differ from the mmp",
"recommend": "apply one account-wide window setting to every ad group and match it in the mmp before comparing campaigns"
},
"audit_optimization_event_health": {
"check": "for each active campaign, confirm its optimization event has been received from the events api in the last 7 days, matches the pixel event name, and reached about 50 events in the window",
"flag": "any campaign optimizing for an event the events api has not delivered in 7 days, an event name mismatch between sources, or under 50 events in 7 days",
"recommend": "align event names, restore the server path, consolidate ad groups to reach the event floor, and hold bid or budget changes until the event flows for 7 days"
},
"monitor_event_freshness": {
"check": "daily, read the delay between order time and events api receipt for complete payment, and confirm test_event_code is absent from production payloads",
"flag": "median delay above one hour, any gap of more than 24 hours with orders in the store, or a test code in production",
"recommend": "send events in real time from the webhook, remove the test code, and alert on gaps before performance is judged"
}
}
}
the bottom line
tiktok trains on what the events api sends and scores it. send both sources, deduplicate on one event_id, put every match key in the server payload, send the purchase from the payment webhook with the real value, and set the attribution windows once for the whole account.
then the number in ads manager, the number in the store, and the number the campaign optimizes on are three views of the same events, and you know why they differ.
built by orbit - the performance marketing intelligence layer. questions? jose@orbitllm.com