Trakero
Developers

Send events in minutes

A 4.5 KB browser SDK for what only the browser knows, and a plain HTTP API for what your server knows for sure.

Quick start

  1. Create a free account. Your store gets a publishable key (browser) and a secret key (server).
  2. Paste the snippet on every page. Your dashboard shows it with your key filled in.
  3. Connect Meta or GA4 under Destinations, then press “Send test event”.
<script>
  window.trakero=window.trakero||function(){(trakero.q=trakero.q||[]).push(arguments)};
  trakero('init', { key: 'pk_live_…' });
</script>
<script async src="https://track.yourstore.com/sdk/v1/trakero.js"></script>

Calls made before the script loads are queued and replayed in order. The script is async and never blocks rendering.

Browser SDK

CallWhat it does
trakero('init', { key })Starts the SDK and sends a page_view (autoPageView: false to disable). Options: endpoint, cookieDomain, cookieDays (365), consent, debug, tags (load dashboard pixels, default true), keeper (cookie keeper URL), dataLayer (read GA4 dataLayer events).
trakero('page')Sends page_view with URL, title and referrer.
trakero('track', name, properties?)Any standard or custom event. Names: letters, digits, _ . : - (max 64).
trakero('viewItem', item, currency?)view_content with the item's value.
trakero('addToCart', items, currency?)add_to_cart; value is computed from price × quantity.
trakero('beginCheckout', ecommerce)begin_checkout.
trakero('addPaymentInfo', ecommerce)add_payment_info.
trakero('purchase', ecommerce)purchase, sent immediately. Requires order_id, value, currency. event_id defaults to purchase_{order_id}.
trakero('identify', identity)Email, phone, name, city, country, external_id, user_id. Kept in memory for the page only, hashed on arrival.
trakero('consent', { ads, analytics })granted | denied | unknown. With ads denied, no ad cookies are created and ad destinations receive nothing.

Cookies are first-party on your domain: _cdt_vid (visitor), _cdt_sid (30-minute session), _cdt_clk (ad click ids), _cdt_ft/_cdt_lt (first and last touch), plus _fbp/_fbc when ads consent is not denied. Events that fail to send (offline, 429, 5xx) are kept and resent on the next page.

For bundlers: import { createTrakero } from "@trakero/browser".

Server events API

POST /v1/events on your tracking domain, with Authorization: Bearer sk_live_…. Send one event object, or { "events": [ … ] } with up to 50. Secret keys are refused when a browser Origin header is present, so they cannot be used from a web page.

POST https://track.yourstore.com/v1/events
Authorization: Bearer sk_live_…
Content-Type: application/json
Idempotency-Key: order-10023-confirmed        (optional)

{
  "event_name": "purchase",
  "event_id": "purchase_10023",
  "occurred_at": "2026-09-27T13:45:17Z",
  "source": "server",
  "visitor_id": "<_cdt_vid cookie from the order>",
  "context": { "ip": "<shopper ip>", "user_agent": "<shopper user agent>" },
  "attribution": { "fbp": "<_fbp>", "fbc": "<_fbc>", "ga_client_id": "<from _ga>" },
  "identity": { "email": "buyer@example.com", "phone": "01712345678", "country": "BD" },
  "consent": { "ads": "granted", "analytics": "granted" },
  "ecommerce": {
    "order_id": "10023", "value": 2580, "currency": "BDT", "shipping": 80,
    "payment_method": "cod",
    "items": [{ "item_id": "SKU-RED-M", "name": "Red Dress", "price": 1250, "quantity": 2 }]
  }
}

You may also send pre-hashed email_sha256 / phone_sha256 (64 hex characters). Test keys (pk_test_, sk_test_) mark every event as TEST.

Server SDKs

Thin clients over the events API: they set event_id to purchase_{order_id}, read the browser SDK's cookies from the request so the ad click travels with the order, and retry 429/5xx/network errors with an idempotency key. See Next.js and Node.js and PHP and Laravel.

// Node.js / Next.js  (@trakero/node)
import { Trakero, shopperFromRequest } from "@trakero/node";
const trakero = new Trakero({ secretKey: process.env.TRAKERO_SECRET_KEY!, endpoint: "https://track.yourstore.com" });
await trakero.purchase({ orderId: "10023", value: 2580, currency: "BDT", customer: { phone: "01712345678" }, shopper: shopperFromRequest(req) });
// PHP / Laravel  (trakero/trakero-php)
$trakero = new TrakeroClient(getenv('TRAKERO_SECRET_KEY'), 'https://track.yourstore.com');
$trakero->purchase(['order_id' => '10023', 'value' => 2580, 'currency' => 'BDT'], ['phone' => '01712345678'],
    TrakeroBrowserContext::shopperFromGlobals());
// .NET / ASP.NET Core  (Trakero.Client)
var trakero = new TrakeroClient(new TrakeroOptions { SecretKey = config["Trakero:SecretKey"]!, Endpoint = "https://track.yourstore.com" });
var shopper = Shopper.From(ctx.Connection.RemoteIpAddress?.ToString(), ctx.Request.Headers.UserAgent, ctx.Request.Headers.Referer, ctx.Request.Headers.Cookie);
await trakero.PurchaseAsync("10023", 2580m, "BDT", customer: new Customer(Phone: "01712345678"), shopper: shopper);

Google Tag Manager and the dataLayer

With dataLayer: true the SDK reads GA4 ecommerce events your site or GTM container already pushes (both dataLayer.push({ event, ecommerce }) and gtag("event", …)), including pushes made before it loaded. A purchase needs transaction_id; it becomes purchase_{transaction_id} so it merges with your server's copy. In GTM, paste this into a Custom HTML tag fired on Initialization - All Pages. Pass a string instead of true to read an array with another name. See Google Tag Manager.

<script>
  window.trakero=window.trakero||function(){(trakero.q=trakero.q||[]).push(arguments)};
  trakero("init", { key: "pk_live_…", dataLayer: true });
</script>
<script async src="https://track.yourstore.com/sdk/v1/trakero.js"></script>

Don't also call trakero("purchase", …) for the same events, or the non-purchase events would be counted twice.

Webhooks

Add a Webhook destination to receive every routed event as JSON at your own https endpoint (your ERP, a data warehouse, a Slack bot). Customer data is only the SHA-256 hashes. Private and internal addresses are refused. Each request carries Trakero-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of {t}.{raw body} with your signing secret. Reject requests whose t is more than 5 minutes old. Answer 2xx; 5xx and timeouts are retried, 410 pauses the destination.

// Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string, secret: string) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
// PHP
function trakero_verify(string $raw, string $header, string $secret): bool {
    parse_str(str_replace(',', '&', $header), $p);
    if (abs(time() - (int) $p['t']) > 300) return false;
    return hash_equals(hash_hmac('sha256', $p['t'] . '.' . $raw, $secret), $p['v1']);
}

Standard events

page_view, view_content, search, add_to_cart, remove_from_cart, view_cart, begin_checkout, add_payment_info, purchase, refund, cancel, lead, generate_lead, sign_up, login, contact, complete_registration, subscribe, start_trial, add_to_wishlist. Any other name is a custom event.

EventRequired
purchaseecommerce.order_id, ecommerce.value ≥ 0, ecommerce.currency (ISO-4217)
refundecommerce.order_id. Partial refunds: a distinct event_id per refund, with value and items.

Deduplication

A purchase is identified by purchase:order:{order_id}; other events by {event_name}:id:{event_id}. Exact repeats from the same source within 48 hours are dropped at ingestion (the response counts them in duplicates). Copies from different sources (browser and server) are merged into one event with several receipts. Destinations always receive the same logical event_id, and GA4 the order id as transaction_id, so their own deduplication agrees. See Pixel + CAPI deduplication.

Limits

LimitValue
Request body256 KB
Single event64 KB
Events per request50
ecommerce.items200
properties50 keys
String fields2,048 characters (URLs 4,096)
occurred_atno older than 7 days, no more than 5 minutes ahead

Responses and errors

A valid request returns 202 with per-event results. An invalid event is rejected on its own; the rest of the batch is accepted.

202 Accepted
{ "request_id": "…", "accepted": 1, "duplicates": 0,
  "errors": [ { "index": 1, "code": "missing_order_id", "message": "purchase requires ecommerce.order_id." } ] }
Status · codeMeaning and what to do
400 invalid_json / empty_bodyThe body is not JSON. Fix the request.
400 invalid_batchSend between 1 and 50 events.
401 missing_key / invalid_keySend a publishable key (browser) or a secret key (server). Check it has not been revoked.
401 secret_key_in_browserA secret key was sent from a web page. Use the publishable key in browsers and rotate the exposed secret key.
401 key_hostname_mismatchThe key belongs to a different store than this tracking domain.
403 origin_not_allowedThe page's origin is not in the store's allowed origins (Settings).
403 tenant_inactiveThe store is not active.
413 payload_too_largeBody over 256 KB. Send smaller batches.
429 rate_limitedSlow down; honour Retry-After.
503 overloaded / queue_unavailableTemporary. Retry after Retry-After seconds. Server SDKs should always retry these.
Per-event codeMeaning
invalid_event_nameLetters, digits, _ . : - only, max 64.
missing_order_id / missing_currency / invalid_valuePurchase requirements not met.
invalid_currencyUse an ISO-4217 code such as BDT or USD.
invalid_event_id / invalid_order_idToo long.
invalid_itemNegative price or quantity.
too_many_items / too_many_propertiesOver the limits above.
occurred_at_too_oldMore than 7 days in the past.
unsupported_event_versionSend event_version 1 or omit it.