Skip to main content
Something happened that the player never saw. A lead form was filled in. A quiz was completed. An upsell was accepted three pages later. Send it to TrackPlay and it lands on the same session as the play, so you can ask the only question that matters: did the people who did this buy?
A custom event is not a conversion. value is reported on its own (count, sum, rate) and never feeds revenue, ROAS, EPC or RPV. Money goes through the Conversions API.

Two ways in

No token. The page already has a session, so the player sends the event for you. Use this for anything that happens on the page.The event name must be declared first. See Declare the event name below. An undeclared name is rejected.On a page with a TrackPlay player:
On a page without a player, load the standalone script:
value is the numeric value. Every other key becomes a property, with one exception: an identity field.You can tie the viewer on the event itself. Pass any of email, email_sha256, email_md5, phone, phone_sha256, phone_md5 or external_id in the same call, and TrackPlay ties this viewer to their profile, the same as a dedicated identify. Send the hash when you would rather the raw address never leave the page:
An identity field is never stored as a property. A raw email is hashed before it leaves the page, and the identity fields are stripped from the event, so a plaintext address is never written as event data. Everything else in the call still lands in properties.

Declare the event name

Before the browser can send an event, the name has to exist as a custom metric.
1

Open custom metrics

Go to Analytics → Custom metrics in your workspace.
2

Add the metric

Create a metric on the custom event and give it the exact name you will send, e.g. quiz_completed.
An undeclared name sent from the browser is rejected, not quietly stored. This is deliberate: the browser path is untrusted, and letting any page mint new event names would let a stranger fill your reports with junk.
The backend API does not require this: a token is already proof of trust. It does still enforce the name cap.

The identity fields

The whole point is landing the event on a real viewer. Pass whichever of these you have. TrackPlay tries them in order and stops at the first that resolves a known viewer. For the concepts behind them, see Viewer identity.
string
The viewer’s session. Best: the exact viewer, with their attribution. This is the same mechanism a conversion postback uses.
string
The viewer’s browser, across visits. A fallback when you have no session.
string
A profile you already resolved. A fallback.
string
Raw email. Resolved through the identity graph, and only a match if that address was linked to a viewer earlier (a captured lead, an earlier sale). Hashed on arrival to both SHA-256 and MD5 with sha256(lower(trim(email))), and never stored raw.
string
A pre-hashed email: sha256(lower(trim(email))), 64 lowercase hex characters. Send the hash when you would rather the raw address never leave your servers.
string
A pre-hashed email: MD5 of lower(trim(email)), 32 lowercase hex characters.
string
Raw phone. Normalized to E.164 on the server (+<country><subscriber>) and then hashed to both SHA-256 and MD5. A number with no + and no leading 00 is dropped, because its country is ambiguous. Never stored raw.
string
A pre-hashed phone: SHA-256 of the number in E.164 form, 64 lowercase hex characters.
string
A pre-hashed phone: MD5 of the number in E.164 form, 32 lowercase hex characters.
string
Your own id for the customer. Resolved through the identity graph, same as email.
A raw email or phone bridges the two hash algorithms: TrackPlay computes both the SHA-256 and the MD5 and writes both, so a viewer later seen through a SHA-256 only platform and one seen through an MD5 only platform unify. Two pre-hashed values sent in separate calls (one SHA-256, one MD5) do not merge on their own. See Viewer identity. Sending a field TrackPlay does not know is rejected with 400 INVALID_PAYLOAD.
Whether you carry these fields on an event (browser track() above, or this backend call) or send them alone through the Identify API, the effect on identity is the same: the viewer ties to their profile. What they do not do is store a raw email or phone for outreach. A hashed value ties the person, it cannot be emailed or called. To store a real contact, send a raw email with a video_code, which upserts a lead so Audience and Hot Leads show an address instead of “Email (hashed)”.
The response tells you which one worked:
matched_by is one of session_id, device_id, profile_id, or identity (an email or external_id resolved through the graph). It is null when nothing matched.

Getting the ids into your server

This is the question every S2S integration hits: the event fires on your backend, but the session and device ids live in the browser. You bridge them. Do it on the funnel domain, where the ids are readable, then carry them to your call.
1

Read the ids on the page

On a page with a player:
On a page without a player, the standalone script exposes the session:
Both read the same first-party cookies (trackplay_session_id, trackplay_device_id) the player set. You can read those cookies directly if you prefer.
2

Hand them to your backend

Put the ids in a hidden form field, your server session, or the payload of your own AJAX call. Then include session_id (and device_id as a fallback) in the request to /v1/event.

Carrying the session across a domain

When checkout is on another domain (clickbank.com), the funnel’s first-party cookie is unreadable there. Carry the session in the URL and the player adopts it on landing:
The player reads ?tp_sid= and joins the same session instead of minting a new one, so an event fired from the checkout page still lands on the viewer who watched.
Where a cart’s buy URL supports a passthrough or vendor parameter (ClickBank’s vendor variables, ElasticFunnels), carry the session through it. Where it does not, do not fight it: identify by email instead. If the buyer was linked to that address earlier (an opt-in, an upsell page), the profile still connects.

When the browser is long gone

An affiliate-network postback arrives minutes later, server to server, with no browser context at all. Attach it by the identity you already hold. A cart conversion attaches by the session id you carried through checkout (see the Conversions API and how a sale finds its video). A custom event can attach by email or external_id: the same viewer was linked to that address on an earlier opt-in, so the profile connects even without a session.

When nothing matches

You get 202 and "attributed": false. The event is still recorded. It lands in the Unattributed bucket, with no session identity attached at all. It is never dropped, and never guessed at. A fabricated session would corrupt your traffic sources and affiliate attribution, which is worse than an honest gap.
A 202 means your integration is not passing a resolvable identifier. It is a signal to fix the plumbing, not a failure to retry. Retrying the same payload will produce the same 202.

Do not double count

Send an event_id. It is your idempotency key: a retry carrying the same event_id collapses into the same row rather than counting twice.
Without an event_id, delivery is at-least-once: if your job retries, the event is counted twice. Use your own primary key: an order ID, a submission ID, anything stable for that one real-world event.
Deduplication holds for 24 hours from the first time we see an event_id. A duplicate arriving later than that (far outside any sane retry schedule) will count again.

Keep the name set small

quiz_completed is an event name. quiz_completed_8f21ba is a bug. A workspace may use 100 distinct event names. Past that, new names are rejected with 422 TOO_MANY_EVENT_NAMES. Names are a grouping dimension across tens of millions of rows; an integration that mints one per visitor destroys every report built on them.
Put the varying part in properties, never in the name: {"event": "quiz_completed", "properties": {"quiz_id": "8f21ba"}}
Names are normalised on arrival: lowercased, reduced to [a-z0-9_-], truncated to 64 characters. Quiz Completed! and quiz completed both become quiz_completed.

Rate limits

Over the limit returns 429 RATE_LIMITED. Back off and retry: with your event_id, the retry is free.

Errors

A field failed validation, or you sent a field we do not know. Unknown fields are rejected rather than ignored, so a typo like sessionId fails loudly instead of silently costing you attribution.
event contained nothing usable after normalisation (e.g. "!!!"). We reject rather than invent a name for you.
Your token lacks events:write. The legacy tp_ workspace key always lands here: it is rejected on purpose. See Authentication.
You have hit the 100-name cap. The response tells you how many names are in use.
Too many events for this workspace or IP.
We could not write the event. Retry with the same event_id.
Token verification is temporarily unavailable. Your token is fine. Retry shortly.

Where the data shows up

Custom events land alongside player events on the same session. Once a name has been declared as a custom metric in Analytics → Custom metrics, you can:
  • chart it on the video’s metric cards,
  • use it as the goal metric of a split test, so the winner is decided on your event rather than a generic play,
  • segment retention by whether the viewer fired it.
A backend event whose name has never been declared as a custom metric is still stored, but nothing in the dashboard is grouped by it yet. Declare the metric to see it.