Customer Data Platform
Tracking events
Stream behavioral events into the CDP with the public, write-only tracking endpoint.
POST /v1/events/track is a public, write-only endpoint built to be called directly from a customer's own website — it can accept events but can't read anything back, so it's safe to call with just a public key, unlike your main API key.
Track a single event
curl -X POST https://api.wezend.com/v1/events/track \
-H "Content-Type: application/json" \
-d '{
"public_key": "wz_pk_...",
"event": "product_viewed",
"identifiers": { "email": "anna@example.com" },
"properties": { "sku": "DK-1180", "price": 499 }
}'
A single event returns 202 { "ok": true }.
Events attach to a contact profile via identifiers (email or phone) — an event without identifiers (or an anonymous_id) is accepted but can never be linked to a contact profile.
Batch events
{
"public_key": "wz_pk_...",
"events": [
{ "event": "product_viewed", "identifiers": { "email": "anna@example.com" }, "properties": { "sku": "DK-1180" } },
{ "event": "add_to_cart", "identifiers": { "email": "anna@example.com" }, "properties": { "sku": "DK-1180", "qty": 1 } }
]
}
returns 202 { "accepted": 2, "rejected": 0 }.
Origin restriction
You can pass the key as X-ZC-Key instead of in the body. Each public key can optionally be restricted to an allowlist of origins, so a leaked key can only be used to post events from your own domain(s) — an empty allowlist means any origin. GET /v1/account/tracking-key returns that key together with a ready-to-paste <script> snippet, and PUT /v1/account/tracking-origins sets the allowlist (e.g. ["https://shop.example.com"]).
Identifying a named contact
Event tracking names nobody, so it needs no signature: the public key identifies your account and nothing else. Anything that speaks for a named contact does — the in-app inbox (POST /v1/inapp/feed, POST /v1/inapp/read) and attaching a push device to a contact (POST /v1/push/subscribe with an email). The public key sits in your page source, so on its own it can never be proof that this visitor is a particular one of your contacts.
Those calls take a signature your own server produces — user_hash = HMAC-SHA256(lowercase(trim(email)), your identity secret), hex:
// your backend, when rendering the page — never in browser code
const crypto = require("crypto");
const email = user.email.trim().toLowerCase();
const userHash = crypto.createHmac("sha256", process.env.WEZEND_IDENTITY_SECRET)
.update(email).digest("hex");
// hand userHash to the page next to the email, then: wezend.identify(email, userHash)
Lower-case and trim the address before hashing. We do the same, so Anna@Firma.dk and anna@firma.dk verify as the same contact — the lookup behind the signature is case-insensitive too. Sign the raw string instead and a user who typed a capital letter gets a 403 that looks like our fault.
The secret comes from GET /v1/account/tracking-identity (dashboard: Settings → Tracking) and is not the public key — keep it on your server. POST /v1/account/tracking-identity/rotate issues a new one and invalidates every hash already computed, so deploy the new secret before you rotate. A call that needs a signature and arrives without one is rejected with 403 and identity_required: true. Push registration can skip it by omitting email — the device is then anonymous. Turning the requirement off entirely (PUT /v1/account/tracking-identity with { "require_identity": false }) means anyone who reads your page source can fetch any contact's messages by supplying their address, so only do it if you have no identified traffic.
What happens next
Once events land, computed traits recompute and segment membership updates in real time — any journey watching that trigger or segment can fire immediately, no batch job involved.
One platform. Every customer interaction.
Replace your patchwork of messaging APIs, CDP and automation tools with a single engagement platform built for scale.
No credit card required · EU data residency · 99.99% uptime SLA