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
- From the browser
- From your backend
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:An identity field is never stored as a property. A raw
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: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.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)”.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:
?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.
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 byemail 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 get202 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.
Do not double count
Send anevent_id. It is your idempotency key: a retry carrying the same event_id
collapses into the same row rather than counting twice.
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"}}[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
400: INVALID_PAYLOAD
400: INVALID_PAYLOAD
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.400: INVALID_EVENT_NAME
400: INVALID_EVENT_NAME
event contained nothing usable after normalisation (e.g. "!!!"). We reject rather
than invent a name for you.403: INSUFFICIENT_SCOPE
403: INSUFFICIENT_SCOPE
Your token lacks
events:write. The legacy tp_ workspace key always lands here: it
is rejected on purpose. See Authentication.422: TOO_MANY_EVENT_NAMES
422: TOO_MANY_EVENT_NAMES
You have hit the 100-name cap. The response tells you how many names are in use.
429: RATE_LIMITED
429: RATE_LIMITED
Too many events for this workspace or IP.
500: WRITE_FAILED
500: WRITE_FAILED
We could not write the event. Retry with the same
event_id.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.
