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
- Create a free account. Your store gets a publishable key (browser) and a secret key (server).
- Paste the snippet on every page. Your dashboard shows it with your key filled in.
- 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
| Call | What 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.
| Event | Required |
|---|---|
| purchase | ecommerce.order_id, ecommerce.value ≥ 0, ecommerce.currency (ISO-4217) |
| refund | ecommerce.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
| Limit | Value |
|---|---|
| Request body | 256 KB |
| Single event | 64 KB |
| Events per request | 50 |
| ecommerce.items | 200 |
| properties | 50 keys |
| String fields | 2,048 characters (URLs 4,096) |
| occurred_at | no 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 · code | Meaning and what to do |
|---|---|
| 400 invalid_json / empty_body | The body is not JSON. Fix the request. |
| 400 invalid_batch | Send between 1 and 50 events. |
| 401 missing_key / invalid_key | Send a publishable key (browser) or a secret key (server). Check it has not been revoked. |
| 401 secret_key_in_browser | A secret key was sent from a web page. Use the publishable key in browsers and rotate the exposed secret key. |
| 401 key_hostname_mismatch | The key belongs to a different store than this tracking domain. |
| 403 origin_not_allowed | The page's origin is not in the store's allowed origins (Settings). |
| 403 tenant_inactive | The store is not active. |
| 413 payload_too_large | Body over 256 KB. Send smaller batches. |
| 429 rate_limited | Slow down; honour Retry-After. |
| 503 overloaded / queue_unavailable | Temporary. Retry after Retry-After seconds. Server SDKs should always retry these. |
| Per-event code | Meaning |
|---|---|
| invalid_event_name | Letters, digits, _ . : - only, max 64. |
| missing_order_id / missing_currency / invalid_value | Purchase requirements not met. |
| invalid_currency | Use an ISO-4217 code such as BDT or USD. |
| invalid_event_id / invalid_order_id | Too long. |
| invalid_item | Negative price or quantity. |
| too_many_items / too_many_properties | Over the limits above. |
| occurred_at_too_old | More than 7 days in the past. |
| unsupported_event_version | Send event_version 1 or omit it. |