Sending events
Everything you send to Tocadule flows through one function:
tocadule.track(eventName, properties)eventName is a string, properties is a plain object. That's the whole
surface. Cart state has its own call (tocadule.cart(...)) and identity has
tocadule.identify(...), but browsing, checkout, purchase, search and any
custom signal all go through track.
Installing the snippet
Paste both tags into your storefront's <head>. The first tag defines a tiny
stub so that tocadule.track(...) exists immediately; the second loads the real
snippet asynchronously.
<script>window.tocadule=window.tocadule||{};window.tocadule._q=window.tocadule._q||[];['track','identify','cart','requestPush'].forEach(function(m){window.tocadule[m]=window.tocadule[m]||function(){window.tocadule._q.push([m,arguments])}});</script>
<script async data-workspace="YOUR_WORKSPACE_ID" src="https://app.tocadule.com/snippet.js"></script>The stub queues every call into window.tocadule._q. When snippet.js finishes
loading it drains the queue and replays each call in order. This means template
code that runs before the async snippet loads — a cart script, an
order-success script — never no-ops. You do not need to wrap calls in a
setTimeout or a "snippet ready" check.
The manual code samples below still guard with if (window.tocadule) because
they can be pasted on pages where the stub tag may be absent — the guard is
harmless when the stub is present.
The payload you send vs. what we store
You only ever supply two things: the event name and a properties object. The
snippet wraps that into the request to /events/track:
// What the snippet actually POSTs — you never build this yourself:
{
workspace_id: ws, // from data-workspace
anonymous_id: aid, // browser identity, managed by the snippet
event: eventName, // your first argument
properties: props, // your object + auto-enrichment (below)
consent_state: "...", // analytics lane, auto-detected
ad_consent: "..." // advertising lane, auto-detected
}workspace_id, anonymous_id, consent_state and ad_consent are all filled
in for you. Your job is the event name and the properties.
Properties: numbers must be numbers
Money and counts must be sent as JavaScript numbers, not strings. If your
platform exposes values as template tokens (e.g. {productPrice}), coerce them
with parseFloat / parseInt before sending. A price stored as the string
"199,90" will not aggregate into revenue correctly.
| Property | Type | Notes |
|---|---|---|
price, cart_total, revenue, amount | number | `parseFloat(...) |
quantity, items_count | number | parseInt(...) (e.g. ` |
product_id, order_id, sku | string | keep as strings — IDs are not math |
currency | string | ISO code, e.g. 'TRY' |
name, brand, category | string | free text |
Guard unreplaced template tokens
If you paste a snippet with {...} placeholders that your store's template
engine fills in server-side, an unreplaced token (the literal string
{productId}) must never be sent. Every standard script uses the same tiny
sub() helper: it passes a value through only if it's non-empty and contains no
{, otherwise it returns undefined (and an undefined property is simply
omitted).
var sub = function(v){ return v && String(v).indexOf('{') < 0 ? v : undefined; };Here is the real product_view script — copy it verbatim and map the {...}
tokens to your platform's variables:
<script>
if (window.tocadule) {
var sub = function(v){ return v && String(v).indexOf('{') < 0 ? v : undefined; };
tocadule.track('product_view', {
product_id: sub('{productId}'),
name: sub('{productName}'),
price: parseFloat('{productPrice}') || 0,
currency: 'TRY',
brand: sub('{productBrand}'),
category: sub('{productCategory}'),
product_image_url: sub('{productImage}'),
product_url: window.location.href,
// Optional — if the shopper is logged in, pass their email. We auto-link the
// contact (no separate identify needed) and match quality on this event jumps.
email: sub('{email}')
});
}
</script>For events that must not fire at all unless key fields are real (like
purchase), guard the whole call. The purchase script only sends once both
order_id and a positive revenue are present, so a page that renders with
unreplaced tokens sends nothing:
<script>
// Put this on your order-success / thank-you page. Replace the {…} tokens with
// YOUR platform's real order variables (any platform — these are placeholders).
// order_id + revenue are REQUIRED. The event only fires once BOTH are real, so
// an un-replaced template never sends a junk order.
// items are OPTIONAL — we fill them from the cart automatically.
// email/phone/name are OPTIONAL but STRONGLY recommended — they auto-link the
// contact and give the purchase the highest match quality (this is the event ad
// platforms optimise toward). shipping / tax / coupon are optional extras.
if (window.tocadule) {
var sub = function(v){ return v && String(v).indexOf('{') < 0 ? v : undefined; };
var num = function(v){ var n = parseFloat(v); return isFinite(n) ? n : 0; };
var orderId = sub('{orderId}');
var revenue = num('{orderTotal}');
if (orderId && revenue > 0) {
tocadule.track('purchase', {
order_id: orderId,
revenue: revenue,
currency: sub('{currency}') || 'TRY',
email: sub('{email}'),
phone: sub('{phone}'),
first_name: sub('{firstName}'),
last_name: sub('{lastName}'),
shipping: num('{shippingTotal}'),
tax: num('{taxTotal}'),
coupon: sub('{couponCode}')
});
}
}
</script>purchase requires order_id and revenue > 0. revenue is the field ad
platforms optimise toward — send it as a number in your store's currency.
Line-items are optional; if you omit them, we backfill them from the visitor's
most recent cart.
What we attach automatically
You send a small object. By the time the event lands, it carries a lot more — none of which you should ever set yourself.
Added in the browser, on every track call:
-
Marketing attribution:
utm_source/utm_medium/utm_campaign/utm_content/utm_term(captured from the URL, persisted 30 min). -
Ad click IDs, nested under
click_ids(captured from the landing URL):Key Where it comes from Kept for gclidGoogle Ads click 90 days gbraid/wbraidGoogle Ads click on iOS (app / web). Safari removes gclidfrom links in private browsing, so these are often the only Google click ID90 days dclidGoogle Display & Video 360 / Campaign Manager click 90 days gad_source/gad_campaignidGoogle Ads campaign markers (they come with most ad clicks, even when the click ID itself was removed) 90 days g_session_attrsGoogle's encoded landing context (the gad_*params, landing URL, referrer, browser). It lets Google match a click that arrived without a usable click ID. Sent once, on the first event after the landing that already has ananonymous_id90 days msclkidMicrosoft Ads click 90 days fbclidMeta click 28 days ttclidTikTok click 28 days epikPinterest click 28 days pinclidPinterest click (older format) 28 days srsltidGoogle Merchant Center free listing. Not an ad click; it only marks the visit as shopping28 days Each key keeps its own clock: a new Meta click does not reset the Google one. If the URL no longer carries an ID (a redirect removed it), the snippet also checks a same-site referrer and, with ad consent, the ad platforms' own cookies (
_gcl_aw,_gcl_gb,ttclid,_epik)._click_tsis the time of the most recent click on any platform, and_click_ts_byholds the time per platform (both in milliseconds). The_fbp/_ttp/_gabrowser-identifier cookies ride along too (read from the merchant's own pixel if present, otherwise baked by us for consented visitors). -
Device:
device_type(mobile / tablet / desktop),browser,os. -
Traffic source:
traffic_source,traffic_platform,referrer_domain. The landing is classified once and the result is kept for the visit (30 minutes of inactivity), so a reload or an in-site page does not turn it intodirect. The first rule that matches wins:traffic_sourceWhen paidThe URL has a Google, TikTok, Microsoft, Pinterest or ChatGPT Ads ( oppref) click ID, agad_source, anfbclidwith a paidutm_medium, or any paidutm_medium(cpc,ppc,paid,cpm,paid_social,display)aiThe referrer is an AI assistant (ChatGPT, Perplexity, Gemini, Copilot, Claude, …) or utm_sourcenames one — organic assistant traffic only; an ad clicked inside ChatGPT ispaidshoppingThe URL has srsltid(a free Google Merchant Center listing)organicA search-engine referrer, or utm_medium=organicreferralAny other external referrer or UTM tag, or an fbclidwithout a paid mediumlikely_aiNo referrer, no campaign tag, and the visitor landed straight on a product page (typical of assistants that hide the referrer) directNone of the above traffic_platformis set when the platform is known:google,meta,tiktok,microsoft,pinterest,chatgptorother. -
Page context:
url,path,title,page_locale,user_language. -
Environment:
screen_width/screen_height,viewport_width/viewport_height,dpr,connection_type.
Added / resolved on the server:
- Contact resolution — the server takes the browser-sent
anonymous_id(see the payload above) and looks up the linked contact through the identity map. This is how anonymous browsing gets stitched to a known shopper. Theanonymous_iditself is generated and sent by the browser, not added server-side. - Location — country, region, city, postal code, timezone (and, only with
analytics consent, latitude / longitude). Resolved from the real client IP via
MaxMind (offline GeoLite2). You do not pass geo; passing your own
country/cityon the event takes precedence if you do. client_ip,user_agent— captured request-side (used to improve ad-platform match quality).- Bot flagging (
is_bot) by user-agent, so crawler traffic is excluded from analytics and never forwarded to ad platforms.
If you set a property we would otherwise auto-fill (say your own url or
country), your value wins. Auto-enrichment only fills gaps.
Dedup keys — you never set them
Tocadule dedups on two keys, and merchants set neither:
anonymous_id— the browser key. The snippet generates and persists it; it rides on every event automatically. It's how repeated events from the same visitor collapse to one identity.source_event_id— the external-system key, used for server-to-server sources (Shopify order webhooks, future integrations) so a replayed webhook can't double-count. Browsertrackcalls do not carry one.
You don't send dedup identifiers on tocadule.track(...). Send the real event;
identity resolution and de-duplication happen behind the scenes.
Attaching a shopper's identity
You can put contact identifiers — email, phone, first_name, last_name —
directly on any event. When an event carries a real email:
- If we don't yet know the visitor, we create the contact and link it to
their
anonymous_id. - If we already know them, we enrich the existing contact.
So you do not need to call identify() before every event. A logged-in
shopper's product_view or add_to_cart that carries their email is a strong,
matchable signal for server-side forwarding to Meta / TikTok / Google.
There are two ways a visitor gets linked to a contact, and they behave differently:
| Path | When to use | Backfill? |
|---|---|---|
tocadule.identify(email, traits) | once, at sign-up / login / opt-in | Yes — links and backfills the visitor's earlier anonymous events |
email on a track event | any event from a known shopper | No — links from that event forward only |
Use identify() once per visitor to also attach marketing permissions (consent_etk or subscribed_email / subscribed_sms, otherwise the contact is never messaged) and profile
fields (first_name, last_name, phone, country, city — the last two
unlock location segments) and to retroactively attribute their prior browsing.
After that, just keep the email on your events.
Never put PII on tocadule.cart(...). Cart state is PII-free by design — it
carries the anonymous_id and resolves to the contact through the identity map.
Any other event carrying the shopper's email links the cart automatically.
Custom events
track accepts any event name. A custom event is inserted just like a
standard one, with all the same auto-enrichment (device, geo, UTM, click IDs,
identity resolution) applied.
tocadule.track('size_guide_opened', { product_id: '101', size_chart: 'shoes-eu' });
tocadule.track('wishlist_add', { product_id: '250' });Custom events appear in your dashboard's Events catalog alongside the
standard ones — grouped by name with total fires, unique contacts, and
last-fired time — and their properties are available for building segments and
automation triggers. What a custom event does not get is the built-in
forwarding map that standard commerce events (product_view, add_to_cart,
checkout_started, purchase, search) have. If you need a custom event to
reach an ad platform, map it explicitly in your destination settings.
Prefer a standard name when one fits — product_view, add_to_cart,
checkout_started, purchase, view_cart, view_item_list, search — so the
event automatically flows into cart intelligence, abandonment automations, and
ad-platform forwarding without extra mapping.
Consent — handled for you
Every event carries two independent consent lanes, both auto-detected by the snippet from your CMP (Google Consent Mode, IAB TCF, OneTrust, Cookiebot, Iubenda, GPC, …):
consent_state— the analytics lane. Gates storing theanonymous_idand PII. We store unless it's explicitlydenied.ad_consent— the advertising lane. Gates whether an event is forwarded to ad destinations (full / limited / modeled).
You don't set these on track. If you have no CMP, you can drive them directly
with tocadule.consent.set('granted'|'denied') and
tocadule.consent.setAd('granted'|'denied'). Email/SMS opt-in
(subscribed_email / subscribed_sms) is a separate merchant preference set via
identify() or your preference center — it is not a CMP lane.