Standard events
These are the events Tocadule expects your store to send. Each one has a canonical name, a set of properties, and a place in the funnel. This page lists every standard event with the exact script you can paste, when to fire it, and which fields are required versus optional.
All events go through the same tiny API on window.tocadule:
tocadule.track(name, props)— record an eventtocadule.cart(data)— set the full current cart (a snapshot, not an event)tocadule.identify(email, traits)— attach a rich profile to the visitortocadule.requestPush(cb)— prompt for web-push permission
Install once, then fire freely
The install block is a small async loader. A stub queues any track / identify /
cart / requestPush calls into window.tocadule._q; the real snippet.js loads
asynchronously and replays that queue on init. This is why your per-page event code
can run before the snippet has finished loading and nothing is lost.
<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>Paste the install block once, site-wide, in your theme header before any per-page
event code. The stub defines window.tocadule synchronously, so the
if (window.tocadule) guard used in the samples below passes right away and any
track / cart / identify calls you make queue into _q and replay once
snippet.js finishes loading — nothing is lost, even on fast-parsing pages (cart,
order-success, sign-up). The failure mode to avoid is firing per-page calls without
the stub in the header: then window.tocadule is undefined when your code runs and the
event is silently dropped.
The code samples below come straight from the in-app "Manual" tab for each event and
guard tokens with a sub() helper — an un-replaced {token} sends as undefined
instead of literal junk. Replace the {…} placeholders with your platform's real
variables.
Identity: put the email on any event
You do not need to call identify() before every event. You can put
email, phone, first_name, last_name, or external_id on any event:
- If we don't yet know the visitor, we auto-create a contact and link it to their
anonymous_id. - If we already know them, we enrich the existing contact.
A visitor needs at least one linking signal to become a known contact. There are two ways to provide it:
| Path | What it does |
|---|---|
identify() once | Links the contact and backfills the visitor's prior anonymous events onto it. |
| Email on an event | Links from that event forward only — it does not backfill earlier anonymous events. |
On browsing events (product_view, add_to_cart, checkout), a logged-in shopper's email lifts match quality (EMQ) — the server-side forward to Meta / TikTok / Google becomes a strong match instead of an anonymous one. Optional identifiers on the events below all auto-link the contact.
Dedup, consent, and geo (how the data is handled)
- Dedup is automatic. Browser events dedup on
anonymous_id; external/webhook events dedup onsource_event_id. You never set a dedup id. - Consent has two independent lanes.
consent_state(analytics/general) gates storing theanonymous_idand PII — we store unless it is explicitlydenied.ad_consent(advertising) gates forwarding to ad destinations (full/ldu/modeled). Email/SMS opt-in is a separate merchant preference, not a CMP lane. - Location on events is resolved server-side from the real client IP (MaxMind,
offline). You don't send geo. A contact's profile location is declared-only — it
comes from
identify()traits, never inferred from IP.
product_view
Fire on a product detail page. Required: product_id. Everything else is optional
context; email (if the shopper is logged in) auto-links the contact.
<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>view_item_list
Fire on a category / collection page. All fields optional.
<script>
if (window.tocadule) {
var sub = function(v){ return v && v.indexOf('{') < 0 ? v : undefined; };
tocadule.track('view_item_list', {
list_name: sub('{categoryName}'),
list_url: window.location.href
});
}
</script>view_cart
Fire when the visitor opens their cart page or drawer. All fields optional; the identifier fields auto-create/link the contact.
<script>
if (window.tocadule) {
var sub = function(v){ return v && String(v).indexOf('{') < 0 ? v : undefined; };
tocadule.track('view_cart', {
cart_id: sub('{cartNumber}'),
cart_url: window.location.href,
cart_total: parseFloat('{total}') || 0,
items_count: parseInt('{totalProduct}') || 0,
coupon: sub('{cartCouponCode}'),
// Optional identifiers — passed on the event auto-create/link the contact.
email: sub('{email}'),
phone: sub('{phone}'),
first_name: sub('{firstName}'),
last_name: sub('{lastName}')
});
}
</script>add_to_cart
Fire when a product is added to the cart. Required: product_id. email (optional)
auto-links the contact.
On most platforms add_to_cart is derived automatically from cart_state
snapshots — we detect additions by diffing the cart. Send it manually only if you
aren't using the cart reader or tocadule.cart().
<script>
if (window.tocadule) {
var sub = function(v){ return v && String(v).indexOf('{') < 0 ? v : undefined; };
tocadule.track('add_to_cart', {
product_id: sub('{productId}'),
name: sub('{productName}'),
price: parseFloat('{productPrice}') || 0,
quantity: parseInt('{quantity}') || 1,
currency: 'TRY',
// Optional — logged-in shopper's email; auto-links the contact.
email: sub('{email}')
});
}
</script>cart_state
The source of truth for the cart. Cart segments, product-in-cart popups, cart
emails, and abandonment automations all read the latest cart_state, and
add_to_cart is derived from it. Send your whole current cart on every change
(add / remove / quantity) using tocadule.cart(…) — not track.
Per item: product_id, name, price (number), and quantity (number) are
required; image and url are optional. Top level: total, currency, and
item_count.
cart_state is PII-free by design. Never put an email or other identifiers on
tocadule.cart() — it carries only cart contents plus the anonymous_id. Once any
other event carries the shopper's email, we link the contact and the cart resolves off
the anonymous_id.
<script>
// Send your WHOLE current cart on EVERY change (add / remove / quantity).
// Build items[] by looping over every line in the cart — one object per line.
// The values below are EXAMPLES — replace them with your real cart data.
// REQUIRED per item: product_id, name, price (number), quantity (number)
// OPTIONAL per item: image, url (make the cart cards look nice)
if (window.tocadule) {
tocadule.cart({
items: [
{ product_id: '101', name: 'Blue T-Shirt', price: 199.90, quantity: 2, image: 'https://yourshop.com/img/blue.jpg', url: 'https://yourshop.com/blue-tshirt' },
{ product_id: '250', name: 'Black Cap', price: 149.00, quantity: 1, image: 'https://yourshop.com/img/cap.jpg', url: 'https://yourshop.com/black-cap' }
// …repeat one object for EACH product currently in the cart
],
total: 549.80, // cart subtotal as a number
currency: 'TRY',
item_count: 3 // total quantity across all lines
// No email / PII here on purpose — the cart stays lean. Once any other event
// carries the shopper's email (purchase, or a logged-in product_view /
// checkout), we link the contact and the cart resolves off the anonymous_id.
});
}
</script>On Shopify, IdeaSoft, T-Soft, and WooCommerce the cart is read automatically — you usually don't need this call. Use it on custom / headless stores or when the automatic reader can't see your cart.
checkout_started
Fire when the visitor reaches the checkout page with items in cart. All fields optional; the identifier fields auto-create/link the contact.
<script>
if (window.tocadule) {
var sub = function(v){ return v && String(v).indexOf('{') < 0 ? v : undefined; };
tocadule.track('checkout_started', {
cart_total: parseFloat('{total}') || 0,
items_count: parseInt('{totalProduct}') || 0,
currency: 'TRY',
// Identifiers on the event auto-create/link the contact — no separate identify.
email: sub('{email}'),
phone: sub('{phone}'),
first_name: sub('{firstName}'),
last_name: sub('{lastName}')
});
}
</script>purchase
Put this on your order-success / thank-you page. Required: order_id and
revenue — the event only fires once both are real values, 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, and coupon are optional
extras.
<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>On Shopify, purchases arrive automatically from the orders/create webhook — you don't
need this script. Use it on non-Shopify stores.
purchase_cancelled
An order was cancelled, refunded, or rejected. It lowers revenue and ROAS by the
reversed amount. It's a back-office event: it never fires in a shopper's browser,
and tocadule.track('purchase_cancelled') is refused (a public page could otherwise
lower a store's revenue). It reaches us one of three ways:
- Shopify — automatic, via the
orders/cancelledandrefunds/createwebhooks. Nothing to add. - Any other platform — from your server, to the server API (below).
- No developer — upload your cancelled-orders export (CSV / Excel) to Toca, or just tell it the order numbers. It shows what it matched before writing anything.
Whichever way, the rules are the same: the order must be one we recorded a purchase
for (same order_id), and an order is reversed at most once — sending the same
cancellation twice, or through two ways, changes nothing. A partial refund is its own
row; a later cancellation reverses only what's left.
Server API: POST /v1/events
Create a key in Settings › Data › API keys (it's shown once — store it like a password, on your server only). Send it as a bearer token:
curl -X POST https://api.tocadule.com/v1/events \
-H "Authorization: Bearer tdk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "purchase_cancelled",
"properties": { "order_id": "1001", "reason": "cancelled" }
}'| Property | |
|---|---|
order_id | Required. The same order id your purchase sent. |
reason | cancelled (default) · rejected · refunded |
amount | The refunded money. Required when reason is refunded; a cancellation reverses the whole order by itself. |
refund_id | Optional — your refund's id, so a retried partial refund is counted once. |
The same door takes any event, not only cancellations:
- one event
{ "event", "properties", "event_id"?, "email"?, "anonymous_id"? }, or{ "events": [ … ] }— up to 100 per request, body ≤ 256 KB; event_idmakes a retry safe (the second send is skipped as a duplicate);emaillinks an existing contact, elseanonymous_id(the visitor's_toc_aid).
The answer lists one result per event, in order — written, skipped (with why:
duplicate, no_purchase, already_reversed) or invalid (with why). A missing or
revoked key is 401; a request whose events are all invalid is 400.
{ "data": { "results": [ { "status": "written" } ] } }identify
Call this once, at sign-up / login / newsletter opt-in. It's the richer one-time
profile call: it sets subscription intent and a fuller profile, and it backfills
the visitor's earlier anonymous events onto the contact. Required: email.
Everything in the traits object is optional — country and city unlock declared
location segments.
<script>
// Call this ONCE, at sign-up / login / newsletter opt-in — it's the richer
// one-time profile call (sets subscription intent + full name/location).
// After this you do NOT need to call identify again: just pass email on your
// events (optional) and we keep the contact linked. Everything except email is
// optional — country + city unlock location segments.
if (window.tocadule) {
tocadule.identify('{email}', { first_name: '{firstName}', last_name: '{lastName}', phone: '{phone}', country: '{country}', city: '{city}' });
}
</script>search
Fire when the visitor runs a site search. Optional: search_term.
<script>
if (window.tocadule) {
var sub = function(v){ return v && v.indexOf('{') < 0 ? v : undefined; };
tocadule.track('search', { search_term: sub('{searchKeyword}') });
}
</script>Custom events
tocadule.track() accepts any event name with any properties. Custom events are
stored verbatim in your event stream exactly as you send them, and they benefit from
the same handling as standard events — dedup on anonymous_id, consent, geo
enrichment, and event-level identity (an email on a custom event still auto-links the
contact).
What's different: a custom name is not part of the required event contract, so it does not map to a standard funnel row or a built-in destination mapping (Meta / GA4), and segment event conditions are built from the canonical names above. Use the standard names whenever one fits — send custom events only for data the standard set doesn't cover.