Kentro Pixel

Kentro Pixel is Kentro's first-party, consent-aware event collection service. It receives events from your websites (through a small browser SDK), from Kentro GO landing pages and from your own servers (through a signed server API), validates them against a shared event taxonomy, applies each visitor's consent, and records them for Kentro analytics, attribution, audiences and destination forwarding.

Preview status. Kentro Pixel v1 is in preview. The collector and SDK host is https://pixel.kentro.live, and the management API is https://apis.kentro.live/pixel. Kentro's staging environment uses the same routes on https://pixel.kentro.dev and https://apis.kentro.dev/pixel for testing. The collector answers immediately; everything that follows (sessions, conversions, the consent record, attribution runs, audience refreshes, destination deliveries and privacy requests) runs asynchronously in Kentro's background processor. See "Processing and retention".

What Kentro Pixel is

  • One collector for every source. Browser events (SDK), Kentro GO landing pages and server-to-server events (HMAC-signed) use the same envelope and the same event taxonomy.
  • Validated at the door. Every event is checked against the JSON Schema of its event name. Unknown names, unlisted properties and floating-point money are rejected per event with a reason code; one bad event never rejects a batch.
  • Consent-aware. Every event carries a consent snapshot. Kentro applies the property's consent rules per purpose (analytics, marketing, functional) and Global Privacy Control, records events that lack the required consent as suppressed (stored minimized) instead of processing them, and checks the person's consent record again before anything is exported.
  • Privacy by default. Page URLs lose their fragment and every query parameter except utm_* and ad click ids, and raw click ids are kept only with marketing consent (otherwise only their SHA-256 hashes). Referrers are reduced to a host name and user agents to a device class and a bot flag (the full user agent is kept only on accepted events with marketing consent, for Meta Conversions API matching). Raw email and phone values are replaced by SHA-256 hashes, and email addresses or phone numbers inside other text are masked. Payloads that look like health data, secrets or card numbers are quarantined. The raw IP address is never stored; ingestion receipts keep a salted hash that rotates daily.
  • Exactly once. Accepted events are deduplicated on event_id per property for at least 14 days, so retries are safe. A suppressed or quarantined event does not use up its event_id, so a corrected or consented resend is accepted.

What Kentro Pixel does not do

  • No fingerprinting. The SDK uses a random first-party anonymous id and a random session id, both kept in memory until analytics consent (or functional storage you declare) allows storing them. Kentro does not combine browser or device attributes into an identifier; the user agent is reduced to a device class and a bot flag.
  • No consent or ad-blocker bypass. Without the required consent an event is suppressed, not processed. If a visitor blocks the SDK or the collector, those visits are missing: Kentro does not model, estimate or fill them in, and analytics responses say so in their coverage notes.
  • No revenue truth from the browser. Orders, refunds, subscriptions and other revenue facts are authoritative only when they arrive from a server source (POST /v1/server/events). Browser copies such as commerce.order_placed are accepted as signals and flagged non-authoritative. Send commerce.order_paid from your backend; that is what revenue reporting trusts.
  • No identity claims from the browser. Browser events cannot set subject_ref or commerce_subject_ref themselves. A signed-in visitor is identified only through an identity token your server signs with a server key (see "Identity tokens for signed-in visitors"); any other claim is dropped and the event is recorded anonymously with IDENTITY_UNVERIFIED.

Hosts at a glance

WhatProductionStaging (testing)Authentication
Collector and browser SDKhttps://pixel.kentro.livehttps://pixel.kentro.devProperty public key, or HMAC server key
Management APIhttps://apis.kentro.live/pixel/v1/...https://apis.kentro.dev/pixel/v1/...Kentro authentication plus the pixel:read or pixel:manage permission

Examples on this page use the production hosts. To test against staging, swap .live for .dev: staging has its own properties, keys and data. See "Hosts and the management API" at the end of this page for authentication details and the full route index.

Quick start

1. Create a property

A property is one place you measure: a website, a store, a Kentro GO landing page, a mobile app or a server source. Each property has its own keys, allowed origins, consent settings and privacy mode.

In Kentro GO, Pixel has its own entry in the navigation (Pixel, visible to users with the pixel:read permission). Its Properties tab creates a property and opens the property page, with the tabs Install, Domains, Keys, Debugger, Health and Settings; Destinations and Privacy are tabs of the Pixel page as well. Every step below also works with the management API. Writes need the pixel:manage permission (admin and pixel:* include it).

curl -sS https://apis.kentro.live/pixel/v1/properties \
  -H "Authorization: Bearer $KENTRO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Main website",
    "type": "website",
    "environment": "production",
    "allowed_origins": ["www.example.com", "*.example.com"],
    "privacy_mode": "standard"
  }'
FieldRequiredNotes
nameyes1 to 200 characters.
typeyeswebsite, shopify, woocommerce, go_page, mobile_app or server.
environmentnoproduction (default), staging or development. Fixed at creation. Production properties get pk_live_ keys, the others pk_test_ keys.
allowed_originsnoUp to 50 host names allowed to send browser events. Empty means any origin. See "Domains and verification".
settingsnoConsent, sensitive path patterns, the health vertical flag, lead retention, sessions and retention. See "Consent and privacy". Sensitive path patterns must use the pattern language of "Sensitive path patterns" and privacy.healthVertical must be true, false or null, otherwise 400.
privacy_modenostandard (default) or strict.

The 201 response contains the property and its first public key:

{
  "property": {
    "id": "0192f0c4-6d1e-7a52-9c1f-3b1f2c7f9a10",
    "name": "Main website",
    "type": "website",
    "environment": "production",
    "allowed_origins": ["www.example.com", "*.example.com"],
    "settings": {
      "consent": { "requireForAnalytics": true, "requireForMarketing": true, "defaultWhenUnknown": "denied", "honorGpc": true },
      "tracking": { "autoPageviews": true, "spaRoutes": true, "clicks": false, "forms": false, "formFieldAllowlist": [], "performance": true, "errors": false },
      "sensitivePathPatterns": [],
      "privacy": { "healthVertical": null },
      "lead": { "rawEmail": false },
      "session": { "idleMinutes": 30, "maxHours": 12 },
      "retention": { "journalMonths": 13, "errorsDays": 30 }
    },
    "privacy_mode": "standard",
    "status": "active",
    "verified_at": null,
    "created_at": "2026-10-09T17:00:00.000Z",
    "updated_at": "2026-10-09T17:00:00.000Z"
  },
  "public_key": {
    "id": "0192f0c4-6d1f-7b33-8d2e-5a9e0b1c2d3e",
    "key": "pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze",
    "label": "default",
    "status": "active",
    "last_used_at": null,
    "created_at": "2026-10-09T17:00:00.000Z",
    "revoked_at": null
  }
}

A public key is meant to be visible in your page source. Restrict where it can be used with allowed_origins, and revoke it with DELETE /v1/properties/:id/keys/:keyId (the key's id) if it leaks into places you do not control. POST /v1/properties/:id/keys creates additional keys, for example one per site.

2. Get the install snippet

curl -sS https://apis.kentro.live/pixel/v1/properties/$PROPERTY_ID/snippet \
  -H "Authorization: Bearer $KENTRO_TOKEN"
{
  "property_id": "0192f0c4-6d1e-7a52-9c1f-3b1f2c7f9a10",
  "public_key": "pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze",
  "snippet": "<script>window.kentroPixel=window.kentroPixel||function(){(window.kentroPixel.q=window.kentroPixel.q||[]).push(arguments)};</script>\n<script async src=\"https://pixel.kentro.live/sdk/v1/kentro-pixel.js\" data-key=\"pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze\"></script>",
  "sdk_url": "https://pixel.kentro.live/sdk/v1/kentro-pixel.js",
  "endpoint": "https://pixel.kentro.live"
}

The snippet uses the oldest active public key of the property; a property without an active key answers 409 NO_ACTIVE_KEY.

3. Install the snippet

Paste the two tags into the <head> of every page:

<script>window.kentroPixel=window.kentroPixel||function(){(window.kentroPixel.q=window.kentroPixel.q||[]).push(arguments)};</script>
<script async src="https://pixel.kentro.live/sdk/v1/kentro-pixel.js" data-key="pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze"></script>
  • The first tag defines window.kentroPixel as a queue: calls made before the SDK has loaded are kept in kentroPixel.q and replayed in order once it arrives.
  • The second tag loads the SDK asynchronously. data-key is the property's public key.
  • When the tag carries data-key and nothing has queued an init call, the SDK initialises itself: it starts a session, sends marketing.session_started and a marketing.page_viewed, and tracks route changes of single-page apps.
  • With the default consent settings, events are recorded as suppressed until your consent banner reports analytics consent. Wire up consentChanged (see "Consent integration") before you judge the install.

4. Verify with the event debugger

Open your site, then check three places:

  1. In the browser's developer tools, the network tab shows POST https://pixel.kentro.live/v1/events answering 202 with one result per event: { "receipt_id": "...", "results": [{ "event_id": "evt_...", "status": "accepted" }] }.
  2. The event debugger lists the newest events Kentro recorded for the property (accepted, quarantined or suppressed), with property values reduced to key names and value types:
curl -sS "https://apis.kentro.live/pixel/v1/events/debug?propertyId=$PROPERTY_ID&limit=20" \
  -H "Authorization: Bearer $KENTRO_TOKEN"
{
  "events": [
    {
      "id": "0192f0c5-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "property_id": "0192f0c4-6d1e-7a52-9c1f-3b1f2c7f9a10",
      "event_id": "evt_192f0c51a2b9f3c7d1e5a2b4c6d8e0f",
      "event_name": "marketing.page_viewed",
      "schema_version": 1,
      "source_type": "browser",
      "environment": "production",
      "occurred_at": "2026-10-09T17:05:00.000Z",
      "received_at": "2026-10-09T17:05:00.412Z",
      "processing_status": "pending",
      "data_classification": "standard",
      "anonymous_id": "anon_6f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e",
      "session_id": "sess_0a1b2c3d4e5f60718293a4b5",
      "refs": { "subject_ref": null, "commerce_subject_ref": null, "order_ref": null, "campaign_ref": null, "creative_ref": null, "landing_page_ref": null, "experiment_ref": null, "experiment_variant_ref": null },
      "context": { "page_url": "https://www.example.com/pricing?utm_source=google", "page_path": "/pricing", "page_title": "Pricing", "referrer_domain": "www.google.com", "utm": { "source": "google" }, "device_class": "desktop", "bot": false, "locale": "en-US", "sdk": { "name": "kentro-pixel-js", "version": "1.1.0" } },
      "consent": { "analytics": "granted", "marketing": "denied", "functional": "unknown", "policy_version": "2026-10", "source": "cmp", "recorded_at": "2026-10-09T17:04:58.000Z", "region": null, "gpc": false },
      "properties_shape": { "page_path": "string", "title": "string" },
      "provenance": { "source_type": "browser", "source_id": "pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze", "schema": "builtin", "is_authoritative": true, "consent_effective": { "analytics": "granted", "marketing": "denied", "functional": "granted" }, "consent_purpose": "analytics", "sdk": { "name": "kentro-pixel-js", "version": "1.1.0" } }
    }
  ]
}

processing_status is pending for accepted events waiting for the background processor, processed once it handled them, suppressed_consent for consent-suppressed events and quarantined for quarantined ones. Consent-suppressed events are shown minimized ("minimized": true): no identifiers, references or property shape, and only the referrer domain, device class, bot flag and locale of the context. Raw ad click ids are never shown, only their hashes (click_id_hashes), and the raw user agent is never shown.

  1. Rejected events never enter the debugger. Ingestion health shows 24-hour counters and the last ten rejections with their reason codes:
curl -sS "https://apis.kentro.live/pixel/v1/ingestion/health?propertyId=$PROPERTY_ID" \
  -H "Authorization: Bearer $KENTRO_TOKEN"
{
  "window_hours": 24,
  "since": "2026-10-08T17:10:00.000Z",
  "receipts": 42,
  "accepted": 120,
  "duplicates": 3,
  "rejected": 2,
  "quarantined": 0,
  "suppressed": 15,
  "freshness": { "last_received_at": "2026-10-09T17:09:58.000Z", "age_seconds": 2 },
  "last_errors": [
    { "received_at": "2026-10-09T16:40:00.000Z", "source_type": "browser", "event_name": "marketing.page_viewed", "reason_code": "SCHEMA_VIOLATION", "reason": "properties must NOT have additional properties", "property_id": "0192f0c4-6d1e-7a52-9c1f-3b1f2c7f9a10" }
  ]
}

You can also validate envelopes before sending them with POST /v1/event-schemas/validate (see "Event envelope and taxonomy").

Domains and verification

Allowed origins

allowed_origins decides which websites may send browser events with the property's public keys. Set it at creation or later with PATCH:

curl -sS -X PATCH https://apis.kentro.live/pixel/v1/properties/$PROPERTY_ID \
  -H "Authorization: Bearer $KENTRO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "allowed_origins": ["example.com", "*.example.com"] }'
  • Up to 50 entries. Each entry is an exact host (shop.example.com) or a wildcard (*.example.com, which matches subdomains at any depth but not the apex example.com; list the apex separately).
  • Entries may be written as full origins (https://shop.example.com:8443); the scheme, port and path are ignored. Matching is by host name, case-insensitive.
  • The collector takes the host from the Origin header, or from the Referer header when there is no Origin (GIF beacons, some legacy calls). With a non-empty allowlist, a request that carries neither header is refused.
  • A request from a host that is not on the list answers 403 ORIGIN_NOT_ALLOWED.
  • An empty list accepts browser events from any origin, except for a Kentro GO landing page property, which then accepts browser events only from Kentro GO's landing page host.
  • The server API is not subject to origin checks; it is authenticated by its signature.
  • Changes reach every collector instance within about a minute (keys and property settings are cached for 60 seconds).

Domain verification

Domain verification proves that you control a domain. Register the domain on the property, publish the token, then ask Kentro to check it.

curl -sS https://apis.kentro.live/pixel/v1/properties/$PROPERTY_ID/domains \
  -H "Authorization: Bearer $KENTRO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "www.example.com" }'
{
  "domain": {
    "id": "0192f0c6-2b3c-7d4e-8f50-6a7b8c9d0e1f",
    "domain": "www.example.com",
    "verification_token": "Jk8x2mQ4Vn7pR1tY5wZ9bC3dF6gH0jL2",
    "verified_at": null,
    "verification_method": null,
    "created_at": "2026-10-09T17:12:00.000Z",
    "instructions": {
      "dns_txt": "TXT www.example.com (or _kentro-pixel.www.example.com) = \"kentro-pixel-verification=Jk8x2mQ4Vn7pR1tY5wZ9bC3dF6gH0jL2\"",
      "http_file": "https://www.example.com/.well-known/kentro-pixel-verification.txt containing Jk8x2mQ4Vn7pR1tY5wZ9bC3dF6gH0jL2",
      "meta_tag": "<meta name=\"kentro-pixel-verification\" content=\"Jk8x2mQ4Vn7pR1tY5wZ9bC3dF6gH0jL2\">"
    }
  }
}

Domains are lower-cased public host names; a leading https://, a path and a port are stripped. IP addresses, localhost and names ending in .local, .internal or .localdomain are refused (400). A domain can be registered once per Kentro workspace (409 Conflict otherwise). Remove one with DELETE /v1/properties/:id/domains/:domainId.

Publish the token with one of three methods:

MethodWhereExact format
dns_txtA TXT record on the domain itself or on _kentro-pixel.<domain>Record value kentro-pixel-verification=<token> (multi-string records are joined before comparing)
http_filehttps://<domain>/.well-known/kentro-pixel-verification.txtA line that is exactly <token> or kentro-pixel-verification=<token>
meta_tagThe HTML of https://<domain>/<meta name="kentro-pixel-verification" content="<token>"> (attribute order and quote style do not matter)

For http_file and meta_tag, Kentro fetches over HTTPS from public addresses only, with a 5 second timeout, does not follow redirects (a redirect counts as not verified), needs a 2xx answer and reads the first 256 KB.

Then verify:

curl -sS https://apis.kentro.live/pixel/v1/properties/$PROPERTY_ID/verify \
  -H "Authorization: Bearer $KENTRO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "method": "dns_txt", "domain": "www.example.com" }'
{ "verified": true, "method": "dns_txt", "domain": "www.example.com", "verified_at": "2026-10-09T17:20:00.000Z", "detail": "TXT record found on www.example.com" }

A failed check answers 422 with "verified": false and a detail such as no TXT record kentro-pixel-verification=... on www.example.com or _kentro-pixel.www.example.com. A domain that is not registered on that property answers 404.

Verification and the allowlist are separate. Verification records ownership (verified_at on the domain and, the first time, on the property). It does not add the domain to allowed_origins and does not gate ingestion: allowed_origins alone decides which websites may send events.

Browser SDK reference

The SDK is a single dependency-free ES2017 file served from https://pixel.kentro.live/sdk/v1/kentro-pixel.js (Cache-Control: public, max-age=3600 with an ETag). It is served unminified so you can read it (about 23 KB; about 11 KB minified). Every method is called through the global function: kentroPixel('method', ...arguments). Once loaded, kentroPixel.version is 1.1.0.

init

kentroPixel('init', {
  key: 'pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze', // default: the script tag's data-key
  endpoint: 'https://pixel.kentro.live',    // default: the origin of the script src
  autoPageviews: true,
  spa: true,
  consent: { analytics: 'unknown', marketing: 'unknown', functional: 'unknown', defaultWhenUnknown: 'denied' },
  campaignRef: 'cmp_spring_launch',
  pageRef: 'page_pricing',
  experimentRef: 'exp_pricing_page'
});
OptionDefaultDescription
keythe tag's data-keyProperty public key.
endpointorigin of the SDK srcCollector base URL. Required when the script tag has no data-key.
autoPageviewstrueSend marketing.page_viewed during init.
spatrueSend a page view on history-based route changes.
consenteverything unknownInitial consent snapshot (see consentChanged). May also carry defaultWhenUnknown.
consent.defaultWhenUnknowndeniedHow the SDK treats unknown analytics consent for its own browser storage (granted or denied). It does not change the collector's decision, which follows the property's settings.consent.
consentProvidernoneA function returning a consent snapshot (or null). Called once during init; its result wins over consent.
functionalStoragefalse (true with the tag's data-functional-storage="true")Declares that your site keeps the SDK's ids as functional storage: they are then stored without analytics consent, until the snapshot says functional: 'denied'.
campaignRefthe tag's data-campaign-refSent as campaign_ref on every event.
pageRefthe tag's data-page-refSent as landing_page_ref on every event.
experimentRefthe tag's data-experiment-refSent as experiment_ref on every event.
  • init runs once; later calls are ignored.
  • Without both a key and an endpoint the SDK stays inactive, silently.
  • When the script tag has data-key and no init call is queued, the SDK calls init itself with the defaults.
  • Queue init before any other call. Queued calls replay in order: page, track, flush and experiment calls placed before init are dropped, and an init replayed after consentChanged replaces that consent with its own.

page

kentroPixel('page', { page_type: 'pricing', page_name: 'Pricing' });

Sends marketing.page_viewed, filling page_path and title from the current page when you leave them out. The event accepts only page_type, page_name, page_path and title; other properties are rejected with SCHEMA_VIOLATION.

track

kentroPixel('track', 'commerce.added_to_cart', {
  product_id: 'sku-123',
  quantity: 1,
  unit_price: { amount_minor: 1999, currency: 'USD' }
}, { campaign_ref: 'cmp_spring_launch' });

track(name, properties?, refs?) sends any builtin or custom event. The optional refs object may set campaign_ref, creative_ref, landing_page_ref, experiment_ref, experiment_variant_ref and order_ref for this event. Person references are set with identify, never through refs.

identify

kentroPixel('identify', identityToken); // minted by your server, see "Identity tokens for signed-in visitors"

Binds the visitor to a person reference without trusting the browser: pass the identity token your server minted for the signed-in user (a string, or { token }). The SDK sends it as identity_token on every later event, and the collector verifies it before it sets subject_ref or commerce_subject_ref. A value that is not token-shaped (two base64url parts joined by a dot, at most 1,024 characters) is ignored by the SDK, and a token that fails verification is dropped by the collector, so raw ids and email addresses never become person references. The token is kept in memory only: call identify on every page load after login, and kentroPixel('identify', null) on sign-out (reset clears it too).

consentChanged

kentroPixel('consentChanged', {
  analytics: 'granted',                  // 'granted' or 'denied'; anything else counts as unknown
  marketing: 'denied',
  functional: 'granted',
  policy_version: '2026-10',             // the version of your consent text
  source: 'cmp',                         // where the choice was made
  recorded_at: '2026-10-09T17:04:58Z',   // default: now
  region: 'US-CA'
});

Every event carries the snapshot in force when the event was created. gpc is filled in by the SDK from navigator.globalPrivacyControl; the purposes stay exactly as you report them. Storage follows the snapshot: when storage becomes allowed the SDK persists its ids (kp_anon, kp_sess, kp_cmp, kp_exp_*); on an explicit denial (analytics: 'denied', or functional: 'denied' when you declared functional storage) it deletes all of them and switches to new in-memory ids, and a later grant persists a new anonymous id, never the old one. Call consentChanged for every change, including withdrawal.

flush

kentroPixel('flush');

Sends the queue now. Without it the SDK sends every 2 seconds, or as soon as 10 events are waiting, with up to 100 events per request.

reset

kentroPixel('reset');

Deletes everything the SDK stored (kp_anon, kp_sess, kp_cmp and kp_exp_*), forgets the identity token, the campaign reference and the experiment assignments, and continues with a new anonymous id and session id (stored only while storage is allowed). Use it on logout or when a visitor asks to start over.

experiment

function applyPricingExperiment() {
  var arm = window.kentroPixel('experiment', 'exp_pricing_page', ['control', 'annual_first']);
  if (arm === 'annual_first') document.body.classList.add('annual-first');
}
if (window.kentroPixel && window.kentroPixel.version) applyPricingExperiment();
else document.querySelector('script[src*="kentro-pixel"]').addEventListener('load', applyPricingExperiment);

experiment(experimentRef, arms) returns the visitor's arm and sends marketing.experiment_exposed (with experiment_ref, experiment_variant_ref and properties.arm) once per session per experiment. The arm is derived from the anonymous id and is sticky: it is stored in kp_exp_<experimentRef> when storage is allowed and kept in memory for the page otherwise. The return value is only available once the SDK has loaded (the queue stub returns nothing), and it is null when the SDK is not initialised, so treat null as your control.

Automatic page views and single-page apps

  • init sends marketing.session_started (entry_url, referrer_domain, landing) when a new session begins, then one marketing.page_viewed unless autoPageviews is false.
  • With spa: true the SDK wraps history.pushState and history.replaceState and listens for popstate. Whenever the URL differs from the last page view it sends a new marketing.page_viewed. URL rewrites through replaceState count too; if your app rewrites the URL often, set spa: false and call page yourself.
  • A session ends after 30 minutes without events. The next event starts a new session id and sends marketing.session_started first.

Data attributes

AttributeEquivalent
data-keyinit({ key })
data-campaign-refinit({ campaignRef })
data-page-refinit({ pageRef })
data-experiment-refinit({ experimentRef })
data-functional-storage="true"init({ functionalStorage: true })

Campaign references are resolved in this order: refs.campaign_ref on the call, then campaignRef or data-campaign-ref, then a utm_id starting with cmp_ on the page URL (Kentro GO campaign links). A cmp_ reference from the URL is reused for the rest of the session.

Storage and lifetimes

NameWhereContentsLifetimeWritten
kp_anonFirst-party cookie (Path=/, SameSite=Lax, Secure on HTTPS) and a localStorage copyRandom anonymous id (anon_ plus 32 hex characters)13 months (390 days)Only while storage is allowed; deleted on an explicit denial
kp_sesslocalStorageRandom session id and two timestampsReplaced after 30 idle minutesOnly while storage is allowed
kp_cmpsessionStorageKentro GO campaign reference taken from utm_idThe browser tab sessionOnly while storage is allowed
kp_exp_<experimentRef>localStorageAssigned experiment armUntil an explicit denial or reset clears itOnly while storage is allowed

Storage is allowed when the snapshot's analytics is granted (or unknown with defaultWhenUnknown: 'granted' in the init options), or when you declared functional storage and functional is not denied. Without it the ids live in memory only, so every page load gets a new anonymous id. Moving from allowed to unknown stops writing without deleting anything; only an explicit denial (or reset) deletes.

Transport and retries

Events go to POST /v1/events with fetch (keepalive, Content-Type: text/plain, so no CORS preflight; the key travels in the body). When the page is hidden or unloaded, the SDK hands the remaining queue to navigator.sendBeacon. Responses 429, 5xx and network errors are retried up to three times with exponential backoff (at most 30 seconds); other 4xx answers are not retried. The in-memory queue holds at most 200 events; when it is full, the oldest are dropped.

Global Privacy Control

  • In the browser, the SDK reports navigator.globalPrivacyControl as gpc: true in every snapshot. It never changes the purposes your consent manager reported.
  • At the collector, consent.gpc: true or the Sec-GPC: 1 request header makes marketing consent denied for that event while settings.consent.honorGpc is true (the default). A marketing event that only GPC blocked is recorded suppressed with GPC_OPT_OUT; an explicit denial stays CONSENT_DENIED. Analytics and functional events are not affected by GPC.
  • Ad networks (Meta Conversions API, Google Ads) never receive a person whose latest consent carries a GPC signal, whatever honorGpc says (see "Destinations").

Consent integration

The SDK does not read your consent manager on its own. Translate your CMP's categories into Kentro purposes, pass the current state at init, and report every change with consentChanged. In this example window.MyCmp and the mycmp:change event stand for your CMP's API:

<script>
  window.kentroPixel=window.kentroPixel||function(){(window.kentroPixel.q=window.kentroPixel.q||[]).push(arguments)};

  function kentroConsentFrom(choices) {
    return {
      analytics: choices.statistics ? 'granted' : 'denied',
      marketing: choices.advertising ? 'granted' : 'denied',
      functional: 'granted',
      policy_version: choices.policyVersion,
      source: 'cmp',
      region: choices.region
    };
  }

  kentroPixel('init', {
    // Called once during init. Return null while the visitor has not chosen yet.
    consentProvider: function () {
      var choices = window.MyCmp && window.MyCmp.getChoices();
      return choices ? kentroConsentFrom(choices) : null;
    }
  });

  // Every later change, including withdrawal.
  window.addEventListener('mycmp:change', function (event) {
    kentroPixel('consentChanged', kentroConsentFrom(event.detail));
  });
</script>
<script async src="https://pixel.kentro.live/sdk/v1/kentro-pixel.js" data-key="pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze"></script>

With a banner you built yourself, call consentChanged from its buttons:

document.getElementById('consent-accept').addEventListener('click', function () {
  kentroPixel('consentChanged', { analytics: 'granted', marketing: 'granted', functional: 'granted', source: 'banner', policy_version: '2026-10' });
});
document.getElementById('consent-reject').addEventListener('click', function () {
  kentroPixel('consentChanged', { analytics: 'denied', marketing: 'denied', functional: 'granted', source: 'banner', policy_version: '2026-10' });
});

No-JavaScript fallback

Where JavaScript is not available, send one envelope (or { "events": [...] }) as base64url-encoded JSON in the e parameter of the GIF endpoint:

<img src="https://pixel.kentro.live/v1/events/pixel.gif?key=pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze&e=eyJldmVudF9uYW1lIjoibWFya2V0aW5nLnBhZ2Vfdmlld2VkIn0" width="1" height="1" alt="">

The answer is always a 200 transparent GIF. The X-Kentro-Pixel-Status response header is accepted when the request was processed and the error code otherwise; per-event results are not returned, so use the event debugger. The e value can be at most 16 KB. The same key, origin, consent and validation rules apply.

Event envelope and taxonomy

The envelope

Browser and server events share one JSON envelope. The SDK fills in ids, context and consent for you; server integrations build it themselves.

{
  "event_id": "evt_192f0c51a2b9f3c7d1e5a2b4c6d8e0f",
  "event_name": "commerce.added_to_cart",
  "schema_version": 1,
  "occurred_at": "2026-10-09T17:00:00.000Z",
  "anonymous_id": "anon_6f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e",
  "session_id": "sess_0a1b2c3d4e5f60718293a4b5",
  "subject_ref": null,
  "commerce_subject_ref": null,
  "order_ref": null,
  "campaign_ref": "cmp_spring_launch",
  "creative_ref": null,
  "landing_page_ref": null,
  "experiment_ref": null,
  "experiment_variant_ref": null,
  "context": {
    "page_url": "https://www.example.com/products/mug?utm_source=google&utm_medium=cpc&gclid=abc123",
    "page_title": "Mug",
    "referrer_domain": "www.google.com",
    "device_class": "desktop",
    "locale": "en-US",
    "sdk": { "name": "kentro-pixel-js", "version": "1.1.0" }
  },
  "consent": { "analytics": "granted", "marketing": "denied", "functional": "granted", "policy_version": "2026-10", "source": "cmp", "recorded_at": "2026-10-09T16:59:00Z", "region": "US-CA", "gpc": false },
  "properties": { "product_id": "sku-123", "quantity": 1, "unit_price": { "amount_minor": 1999, "currency": "USD" } }
}
FieldType and limitsNotes
event_idstring, 1 to 128 charactersUnique per real-world event and stable across retries; deduplicated per property. When omitted, Kentro derives it from a hash of the envelope, so two identical envelopes count once.
event_namestring, 3 to 128 charactersnamespace.name in lower case: letters, then a dot, then letters, digits or underscores.
schema_versioninteger, 1 to 1000Defaults to 1.
occurred_atISO 8601 timestampDefaults to the time of receipt. Rejected with CLOCK_SKEW when more than 7 days in the past or 5 minutes in the future.
anonymous_id, session_idstring up to 128 characters, or nullBrowser identifiers; the SDK fills them.
subject_ref, commerce_subject_refstring up to 256 characters, or nullPseudonymous person references: your login id, your commerce customer id. Kept as sent on server events; on browser events set only through identity_token.
identity_tokenstring up to 1,024 charactersBrowser events: an identity token your server signed (the SDK's identify sets it). Verified on arrival, never stored.
order_ref, campaign_ref, creative_ref, landing_page_ref, experiment_ref, experiment_variant_refstring up to 256 characters, or nullJoin keys for orders, Kentro GO campaigns, creatives, landing pages and experiments.
contextobjectPage and device context, see below.
consentobjectConsent snapshot. A missing purpose is unknown.
propertiesobjectEvent-specific data, validated against the event's schema.

An envelope may be at most 32 KB serialized. Unknown top-level fields are dropped. Kentro adds the workspace, property, source type, environment and receipt time itself.

context accepts page_url, page_title, referrer or referrer_domain, utm (source, medium, campaign, content, term, id), click_ids (gclid, gbraid, wbraid, fbclid, ttclid, msclkid), device_class, bot, locale, timezone, app_version, source_channel, ga4_client_id (your site's GA4 client id, the _ga cookie value, used for GA4 deliveries), client_user_agent (server sources: the visitor's browser user agent, for Meta Conversions API matching) and sdk. On ingestion page_url keeps only utm_* and the click ids from its query string, loses its fragment and credentials, and has sensitive paths redacted (see "Sensitive path patterns"); raw click ids stay only when marketing consent is granted, otherwise they are removed and kept as SHA-256 hashes; the referrer is reduced to its host name; for browser sources device_class and bot are derived from the request's user agent and cannot be set by the client; the full user agent (client_user_agent, or the browser's own for browser sources) is kept only on accepted events whose consent grants marketing. Property values whose key ends in url, uri, href, link or path get the same URL treatment.

Money

Money is always an object with integer minor units and an upper-case ISO 4217 code:

{ "amount_minor": 4999, "currency": "USD" }

4999 is 49.99 USD; for zero-decimal currencies such as JPY, amount_minor is the whole amount. Floats, strings and extra keys are rejected with SCHEMA_VIOLATION.

Email and phone identifiers

Raw email and phone values at the top level of properties or under properties.customer are replaced on ingestion by email_sha256 and phone_sha256 (email trimmed and lower-cased, phone reduced to its digits, then SHA-256 as lower-case hex); the raw value is discarded. The builtin schemas accept identifiers only inside customer, so send properties.customer.email (or a pre-hashed email_sha256), never a top-level email. If you hash yourself, use the same normalisation.

The shared customer object accepts ref, email_sha256, phone_sha256, country (2 characters) and marketing_opt_in (boolean).

Contact details elsewhere are protected too: in custom and legacy events, a top-level property whose name ends like a contact field (customer_email, email_address, phone_number, mobile, ...) is replaced by <name>_sha256; in every event, email addresses inside other strings become [email] and phone numbers inside free text (such as titles, notes or search queries) become [phone].

Standard taxonomy

Every builtin event has an allowlist of properties (additionalProperties is false), so unlisted keys are rejected. Strings are at most 256 characters unless the full schema says otherwise; counts are non-negative integers (quantity and step start at 1); items is an array of up to 200 line items (product_id and quantity required; variant_id, sku, name, category, unit_price and total optional). "Authoritative from" says which sources make the event a fact: Browser or server covers browser, server, Shopify pixel, Kentro OS and Kentro GO landing pages; Server only covers the server API, Kentro OS, CRM and imports. Events from any other source are accepted but flagged is_authoritative: false. The full JSON Schemas are available from GET /v1/event-schemas.

EventCategoryRequired propertiesOther allowed propertiesConsent purposeAuthoritative from
marketing.page_viewedmarketingnonepage_type, page_name, page_path, titleanalyticsBrowser or server
marketing.session_startedmarketingnoneentry_url, referrer_domain, landinganalyticsBrowser or server
marketing.product_viewedmarketingproduct_idproduct_name, category, price (money)analyticsBrowser or server
marketing.search_performedmarketingqueryresults_count, categoryanalyticsBrowser or server
marketing.content_viewedmarketingnonecontent_id, content_type, content_name, legacy_eventanalyticsBrowser or server
marketing.cta_clickedmarketingcta_idcta_text, destination_url, locationanalyticsBrowser or server
marketing.form_startedmarketingform_idform_nameanalyticsBrowser or server
marketing.form_submittedmarketingform_idform_name, fields (field names only), success, customeranalyticsBrowser or server
marketing.lead_capturedmarketingnonelead_ref, form_id, source, customermarketingBrowser or server
marketing.signup_completedmarketingnonemethod, plan, customeranalyticsBrowser or server
marketing.email_opt_in_recordedmarketingnonelist_id, source, double_opt_in, customermarketingBrowser or server
marketing.experiment_exposedmarketingnonearmanalyticsBrowser or server
marketing.asset_engagedmarketingasset_refasset_type, action, secondsanalyticsBrowser or server
commerce.product_viewedcommerceproduct_idvariant_id, sku, name, category, brand, price (money), in_stockanalyticsBrowser or server
commerce.collection_viewedcommercecollection_idname, product_countanalyticsBrowser or server
commerce.added_to_cartcommerceproduct_id, quantityvariant_id, sku, name, unit_price (money), cart_idanalyticsBrowser or server
commerce.removed_from_cartcommerceproduct_id, quantityvariant_id, sku, name, cart_idanalyticsBrowser or server
commerce.checkout_startedcommercenonecart_id, checkout_id, total (money), item_countanalyticsBrowser or server
commerce.checkout_step_completedcommercestepcheckout_id, step_nameanalyticsBrowser or server
commerce.payment_attemptedcommercenonecheckout_id, payment_method, amount (money), successanalyticsBrowser or server
commerce.order_placedcommerceorder_id, total (money)subtotal, tax, shipping, discount (money), items, item_count, coupon_code, payment_method, customerfunctionalServer only
commerce.order_paidcommerceorder_id, total (money)payment_method, transaction_reffunctionalServer only
commerce.order_cancelledcommerceorder_idreasonfunctionalServer only
commerce.order_refundedcommerceorder_id, amount (money)reason, refund_reffunctionalServer only
commerce.order_partially_refundedcommerceorder_id, amount (money)items, reason, refund_reffunctionalServer only
commerce.return_createdcommerceorder_idreturn_ref, items, reasonfunctionalServer only
commerce.subscription_startedcommercesubscription_idplan, amount (money), intervalfunctionalServer only
commerce.subscription_renewedcommercesubscription_idamount (money), period_endfunctionalServer only
commerce.subscription_cancelledcommercesubscription_idreasonfunctionalServer only
commerce.customer_createdcommercecustomernonefunctionalServer only
commerce.customer_updatedcommercecustomernonefunctionalServer only
commerce.fulfillment_shippedcommerceorder_idfulfillment_ref, carrier, tracking_reffunctionalServer only
commerce.fulfillment_deliveredcommerceorder_idfulfillment_ref, delivered_atfunctionalServer only
commerce.shipping_cost_finalizedcommerceorder_id, shipping_cost (money)carrierfunctionalServer only
marketing.campaign_impressionmarketingprovider, impressionsad_refmarketingServer only
marketing.ad_clickedmarketingproviderad_ref, click_id_typemarketingBrowser or server
marketing.lead_qualifiedmarketinglead_refscore, stagemarketingServer only
marketing.demo_bookedmarketingnonelead_ref, scheduled_at, sourcemarketingBrowser or server
marketing.purchase_attributedmarketingorder_ref, modelcredit (0 to 1), campaign_refmarketingServer only
marketing.audience_joinedmarketingaudience_refnonemarketingServer only
marketing.audience_exitedmarketingaudience_refnonemarketingServer only
marketing.conversion_confirmedmarketingconversion_refconversion_type, amount (money)marketingServer only
marketing.campaign_activatedopsnoneprovider, external_reffunctionalServer only
marketing.campaign_pausedopsnoneprovider, external_ref, reasonfunctionalServer only

Custom events and the schema registry

A custom event schema registered for your workspace (an event name, a version, a category and a JSON Schema for properties) is validated exactly like a builtin: the name follows the same namespace.name pattern, schema_version must match a registered version, and a builtin with the same name always wins. Custom events have the functional consent purpose when their category is ops and analytics otherwise, and they are authoritative only when sent through the server API. Registry changes reach the collector within 60 seconds.

  • GET https://apis.kentro.live/pixel/v1/event-schemas (authenticated) answers { "builtin": [...], "custom": [...] } with the builtin taxonomy and your workspace's custom schemas.
  • GET https://pixel.kentro.live/v1/event-schemas (public, cached for 5 minutes) answers the builtins only.
  • Each builtin entry carries event_name, version, category, required, schema, authoritative_sources, consent_purpose, description and builtin: true.
  • Creating custom schemas through the API is not available in this preview.

Validating events without sending them

POST /v1/event-schemas/validate runs the ingestion checks (envelope, clock skew, schema, identity tokens, sensitive-data detectors and the consent decision) on one envelope or { "events": [...] } (1 to 100), evaluated as browser events, and stores nothing. With a property key the property's settings apply; with Kentro credentials the default settings do. It does not check the origin allowlist, the per-key event rate limit or duplicates; requests count against the collector's per-IP limits. Authenticate with a property key (X-Kentro-Pixel-Key header, key query parameter or key in the body) on the collector host, or with your Kentro credentials on the management host.

curl -sS https://pixel.kentro.live/v1/event-schemas/validate \
  -H "X-Kentro-Pixel-Key: pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      { "event_id": "evt_1", "event_name": "commerce.added_to_cart", "consent": { "analytics": "granted" }, "properties": { "product_id": "sku-1", "quantity": 1, "unit_price": { "amount_minor": 1999, "currency": "USD" } } },
      { "event_id": "evt_2", "event_name": "commerce.added_to_cart", "consent": { "analytics": "granted" }, "properties": { "product_id": "sku-1", "quantity": 1, "unit_price": { "amount_minor": 19.99, "currency": "USD" } } }
    ]
  }'
{
  "results": [
    { "index": 0, "event_id": "evt_1", "event_name": "commerce.added_to_cart", "valid": true, "outcome": "accepted", "reason_code": null },
    { "index": 1, "event_id": "evt_2", "event_name": "commerce.added_to_cart", "valid": false, "reason_code": "SCHEMA_VIOLATION", "reason": "properties/unit_price/amount_minor must be integer" }
  ]
}

outcome is accepted, quarantined or suppressed, exactly what ingestion would record.

Server API

Use the server API for facts your backend knows: paid orders, refunds, subscriptions, fulfilment, qualified leads. Events sent this way have source_type: server and are authoritative for every builtin event.

Server keys

Create a server key on the property the events belong to (pixel:manage; in Kentro GO, the property's Keys tab):

curl -sS https://apis.kentro.live/pixel/v1/properties/$PROPERTY_ID/server-keys \
  -H "Authorization: Bearer $KENTRO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "label": "orders backend" }'
{
  "server_key": {
    "id": "0192f0c7-3c4d-7e5f-9061-7b8c9d0e1f2a",
    "key_id": "sk_4fHq8Lm2Tz9Wb1Xc",
    "label": "orders backend",
    "status": "active",
    "last_used_at": null,
    "created_at": "2026-10-09T17:30:00.000Z",
    "revoked_at": null,
    "secret": "kQ9vX2mT7pL4wR8zN1cB6yH3jF5dS0aG2eU7iO9qW4E"
  }
}
  • key_id (sk_ plus 16 characters) identifies the key in requests; secret (32 random bytes, base64url) signs them.
  • The secret is shown only in this response. Kentro stores it encrypted and cannot show it again; if you lose it, create a new key and revoke the old one.
  • Revoke with DELETE /v1/properties/:id/server-keys/:keyId, where :keyId is the key's id (the UUID), not the sk_ key id. Revocation reaches every collector instance within about a minute: requests signed with the key then answer 401 KEY_REVOKED, and identity tokens it signed stop verifying.
  • The same secret signs identity tokens for signed-in visitors (see "Identity tokens for signed-in visitors").
  • A 503 SIGNING_UNAVAILABLE answer means Kentro cannot store secrets at the moment; try again later.

Request

POST https://pixel.kentro.live/v1/server/events

HeaderValue
Content-Typeapplication/json. Required: the signature is checked over the raw JSON bytes, which are only captured for this content type.
X-Kentro-Key-IdThe sk_ key id.
X-Kentro-TimestampUnix time in seconds (digits only).
X-Kentro-Signaturesha256= followed by the lower-case hex HMAC-SHA256 of timestamp + "." + rawBody, keyed with the secret.
X-Kentro-Idempotency-KeyOptional, up to 128 characters. Binds the batch to its body for safe retries (see Idempotency).

The body is { "events": [ ... ] } with 1 to 500 envelopes and at most 512 KB. No origin check applies; device_class and bot can be set in context.

Signing rules:

  • Serialize the body once, sign that exact string, send that exact string.
  • The HMAC key is the secret string as shown (its UTF-8 bytes). Do not base64-decode it.
  • The timestamp must be within 300 seconds of Kentro's clock in either direction (TIMESTAMP_STALE otherwise).
  • The comparison is constant-time; a wrong or missing signature answers 401 SIGNATURE_INVALID.

Idempotency

Two layers protect retries:

  • Per event. event_id is deduplicated per property: re-sending an event that was already accepted answers duplicate for that event and stores nothing new. Only accepted events use up their event_id (a rejected, suppressed or quarantined event does not, so a corrected or consented resend is accepted), and accepted ids are remembered for at least 14 days. Keep event_id stable across retries and unique per real-world event (for example order-1001-paid); without one, Kentro derives it from a hash of the envelope.
  • Per batch. X-Kentro-Idempotency-Key binds the key to the SHA-256 of the exact request body. The same key with the same body answers the original receipt with "replayed": true without evaluating the events again; a replayed receipt carries the first 100 results of the original batch. The same key with a different body answers 409 IDEMPOTENCY_KEY_REUSED, so a retry is never answered with the receipt of another batch. A key stays bound to its first body for as long as the property keeps its event history (at least 13 months, or longer when settings.retention.journalMonths asks for it), so use a new key for every new batch. Keys longer than 128 characters are ignored.

The signature is only valid for 300 seconds, so a captured request can be replayed only inside that window, and the two layers above turn such a replay into duplicate results or a replayed receipt.

Consent on server events

Server events go through the same consent decision as browser events. Revenue and lifecycle facts (commerce.order_*, subscriptions, customers, fulfilment, commerce.shipping_cost_finalized, marketing.campaign_activated and marketing.campaign_paused) have the functional purpose and are accepted unless the snapshot says functional: "denied". Analytics and marketing events sent from your server (for example marketing.lead_qualified or marketing.conversion_confirmed) need the consent you hold for that person in the envelope's consent; with the default property settings and no snapshot they are recorded as suppressed. The Sec-GPC header is ignored on this route; consent.gpc is honoured.

Examples

Each example sends one paid order. The three snippets produce identical signatures for identical bodies.

KEY_ID='sk_4fHq8Lm2Tz9Wb1Xc'
SECRET='kQ9vX2mT7pL4wR8zN1cB6yH3jF5dS0aG2eU7iO9qW4E'
BODY='{"events":[{"event_id":"order-1001-paid","event_name":"commerce.order_paid","order_ref":"order-1001","commerce_subject_ref":"cust-42","properties":{"order_id":"order-1001","total":{"amount_minor":4999,"currency":"USD"},"payment_method":"card"}}]}'
TS=$(date +%s)
SIG="sha256=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)"

curl -sS https://pixel.kentro.live/v1/server/events \
  -H "Content-Type: application/json" \
  -H "X-Kentro-Key-Id: $KEY_ID" \
  -H "X-Kentro-Timestamp: $TS" \
  -H "X-Kentro-Signature: $SIG" \
  -H "X-Kentro-Idempotency-Key: order-1001-paid" \
  --data-binary "$BODY"
const crypto = require('node:crypto');

const KEY_ID = process.env.KENTRO_PIXEL_KEY_ID;     // sk_...
const SECRET = process.env.KENTRO_PIXEL_KEY_SECRET; // shown once when the key was created

async function sendServerEvents(events, idempotencyKey) {
  const body = JSON.stringify({ events });
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const signature = 'sha256=' + crypto.createHmac('sha256', SECRET).update(`${timestamp}.${body}`).digest('hex');

  const res = await fetch('https://pixel.kentro.live/v1/server/events', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Kentro-Key-Id': KEY_ID,
      'X-Kentro-Timestamp': timestamp,
      'X-Kentro-Signature': signature,
      ...(idempotencyKey ? { 'X-Kentro-Idempotency-Key': idempotencyKey } : {}),
    },
    body,
  });
  const result = await res.json();
  if (res.status !== 202) throw new Error(`Kentro Pixel ${res.status}: ${result.error}`);
  return result; // { receipt_id, results: [{ event_id, status, reason_code? }] }
}

sendServerEvents([
  {
    event_id: 'order-1001-paid',
    event_name: 'commerce.order_paid',
    occurred_at: new Date().toISOString(),
    order_ref: 'order-1001',
    commerce_subject_ref: 'cust-42',
    properties: {
      order_id: 'order-1001',
      total: { amount_minor: 4999, currency: 'USD' },
      payment_method: 'card',
    },
  },
], 'order-1001-paid').then((receipt) => console.log(receipt.results));
import hashlib
import hmac
import json
import os
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone

KEY_ID = os.environ["KENTRO_PIXEL_KEY_ID"]      # sk_...
SECRET = os.environ["KENTRO_PIXEL_KEY_SECRET"]  # shown once when the key was created


def send_server_events(events, idempotency_key=None):
    body = json.dumps({"events": events}, separators=(",", ":")).encode("utf-8")
    timestamp = str(int(time.time()))
    digest = hmac.new(SECRET.encode("utf-8"), timestamp.encode("ascii") + b"." + body, hashlib.sha256).hexdigest()
    headers = {
        "Content-Type": "application/json",
        "X-Kentro-Key-Id": KEY_ID,
        "X-Kentro-Timestamp": timestamp,
        "X-Kentro-Signature": "sha256=" + digest,
    }
    if idempotency_key:
        headers["X-Kentro-Idempotency-Key"] = idempotency_key
    request = urllib.request.Request("https://pixel.kentro.live/v1/server/events", data=body, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(request, timeout=10) as response:
            return json.load(response)  # {"receipt_id": ..., "results": [...]}
    except urllib.error.HTTPError as error:
        raise RuntimeError(f"Kentro Pixel {error.code}: {error.read().decode()}") from error


receipt = send_server_events([{
    "event_id": "order-1001-paid",
    "event_name": "commerce.order_paid",
    "occurred_at": datetime.now(timezone.utc).isoformat(),
    "order_ref": "order-1001",
    "commerce_subject_ref": "cust-42",
    "properties": {
        "order_id": "order-1001",
        "total": {"amount_minor": 4999, "currency": "USD"},
        "payment_method": "card",
    },
}], idempotency_key="order-1001-paid")
print(receipt["results"])

Retry guidance: on 429 wait for Retry-After; on 500 and 503 retry with backoff using the same event_id values and idempotency key; on 400 and 401 fix the request first.

Response

A processed request answers 202:

{
  "receipt_id": "0192f0c8-0b1c-7d2e-9f30-4a5b6c7d8e9f",
  "results": [
    { "event_id": "order-1001-paid", "status": "accepted" }
  ]
}

A batch replay adds "replayed": true. results follows the order of the request and uses one of five statuses:

StatusMeaning
acceptedValid and stored for processing.
duplicateAn event with this event_id was already received for the property; nothing new was stored.
rejectedInvalid; not stored. reason_code says why, and the rejection appears in ingestion health.
quarantinedSensitive data detected; stored without its properties, never processed.
suppressedConsent missing or denied for the event's purpose; recorded minimized, never processed.

Per-event reason codes:

CodeStatusMeaning
INVALID_ENVELOPErejectedNot an object, fails the envelope rules (types, lengths, the event_name pattern) or occurred_at is not a date.
PAYLOAD_TOO_LARGErejectedThe event is larger than 32 KB serialized.
CLOCK_SKEWrejectedoccurred_at is more than 7 days in the past or 5 minutes in the future.
UNKNOWN_EVENTrejectedNo builtin or custom schema for this event_name and schema_version.
SCHEMA_VIOLATIONrejectedproperties does not match the schema; reason points at the field.
SENSITIVE_PAYLOADquarantinedHealth terms, ICD-10 codes, secrets or card numbers were detected.
CONSENT_DENIEDsuppressedThe effective consent for the event's purpose is denied.
GPC_OPT_OUTsuppressedGlobal Privacy Control alone denied a marketing event.
SENSITIVE_SCAN_INCOMPLETEquarantinedThe payload was too large or too deeply nested to scan completely.
SENSITIVE_REDACTEDacceptedAuthoritative server commerce events only: the fields that matched a detector were replaced; the order still counts, but the event is never forwarded to destinations.
IDENTITY_UNVERIFIEDacceptedBrowser events: a person reference without a valid identity token was dropped; the event was recorded anonymously.

Request-level errors refuse the whole request. Handler errors answer { "error": "<CODE>", "reason_code": "<CODE>", "message": "..." }; framework errors (400 for unparsable JSON, 413, 415, 500) answer { "error": "<CODE>", "message": "..." }. The same codes apply to the browser endpoints.

HTTPCodeWhenRetry
400INVALID_ENVELOPEThe body is not JSON, not an envelope or { "events": [...] }, or has no events.No
400BATCH_TOO_LARGEMore than 100 (browser) or 500 (server) events; the body carries max.Split the batch
401KEY_INVALIDMissing or unknown key, disabled property, or a key whose environment does not match the property.No
401KEY_REVOKEDThe key was revoked.No
401SIGNATURE_INVALIDMissing or wrong signature, or the body was not sent as application/json.After fixing the signing
401TIMESTAMP_STALEX-Kentro-Timestamp missing, not digits, or more than 300 seconds off.After re-signing with the current time
403ORIGIN_NOT_ALLOWEDThe browser origin is not in allowed_origins.No
409IDEMPOTENCY_KEY_REUSEDServer route: the idempotency key was already used with a different body.With a new key
413PAYLOAD_TOO_LARGEThe body exceeds 64 KB (browser) or 512 KB (server).Split the batch
415UNSUPPORTED_MEDIA_TYPEA content type other than application/json (or text/plain on browser routes).No
429RATE_LIMITEDToo many events for this key, or too many requests (or unknown keys) from this client; the Retry-After header says how many seconds to wait.After Retry-After
500INTERNAL_ERRORUnexpected error.With backoff
503COLLECTOR_DISABLEDThe collector is switched off.Later
503INGEST_UNAVAILABLEStorage is temporarily unavailable.With backoff
503SIGNING_UNAVAILABLEServer key secrets are temporarily unavailable.Later

Identity tokens for signed-in visitors

Browser events cannot claim a person: the public key is visible in every page, so any script could. To bind a signed-in visitor to your subject_ref (your login id) or commerce_subject_ref (your customer id), your server mints a short-lived identity token with one of the property's server keys and hands it to the page, which passes it to kentroPixel('identify', token).

token     = payload + "." + signature
payload   = base64url(UTF-8 JSON { "sub", "typ", "exp", "kid" })   (no padding)
signature = base64url(HMAC-SHA256(serverKeySecret, payload))       (no padding)
ClaimValue
subThe pseudonymous reference: 1 to 256 characters, no whitespace, no @ (never an email address).
typsubject_ref or commerce_subject_ref: the reference the collector sets.
expExpiry in Unix seconds, at most 24 hours after minting.
kidThe sk_ key id of an active server key of the same property as the browser's public key.

The HMAC key is the server key secret exactly as it was shown (its UTF-8 bytes, not base64-decoded), and the signature covers the base64url payload string. A token is at most 1,024 characters.

const crypto = require('node:crypto');

const KEY_ID = process.env.KENTRO_PIXEL_KEY_ID;     // sk_...
const SECRET = process.env.KENTRO_PIXEL_KEY_SECRET; // shown once when the key was created

function identityToken(sub, typ = 'subject_ref', ttlSeconds = 3600) {
  const claims = { sub, typ, exp: Math.floor(Date.now() / 1000) + ttlSeconds, kid: KEY_ID };
  const payload = Buffer.from(JSON.stringify(claims), 'utf8').toString('base64url');
  const signature = crypto.createHmac('sha256', SECRET).update(payload).digest('base64url');
  return `${payload}.${signature}`;
}

console.log(identityToken('usr_8f3a2c91'));
import base64
import hashlib
import hmac
import json
import os
import time

KEY_ID = os.environ["KENTRO_PIXEL_KEY_ID"]      # sk_...
SECRET = os.environ["KENTRO_PIXEL_KEY_SECRET"]  # shown once when the key was created


def b64url(data):
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")


def identity_token(sub, typ="subject_ref", ttl_seconds=3600):
    claims = {"sub": sub, "typ": typ, "exp": int(time.time()) + ttl_seconds, "kid": KEY_ID}
    payload = b64url(json.dumps(claims, separators=(",", ":")).encode("utf-8"))
    signature = b64url(hmac.new(SECRET.encode("utf-8"), payload.encode("ascii"), hashlib.sha256).digest())
    return payload + "." + signature


print(identity_token("usr_8f3a2c91"))

Hand the token to the page the signed-in visitor loads, after the install snippet:

<script>
  kentroPixel('identify', '<identity token minted by your server>');
</script>

What the collector does with a token:

  • It verifies each distinct token of a request once, at most 5 per request: the structure, a server key that exists, is active and belongs to the same property, the signature (compared in constant time) and the expiry (in the future, and no more than 24 hours ahead, with 5 minutes of tolerance for your server's clock).
  • A valid token sets subject_ref or commerce_subject_ref to sub on the events that carry it; any other person reference the browser sent is dropped.
  • A missing or failing token never rejects an event: the person reference is dropped and the event is accepted anonymously with reason_code: IDENTITY_UNVERIFIED.
  • Tokens are never stored. Revoking the server key invalidates every token it signed.

Consent and privacy

Purposes

Every event has one consent purpose, fixed by its schema: analytics, marketing or functional (see the taxonomy table; custom events are functional when their category is ops and analytics otherwise). The envelope's consent snapshot gives a state per purpose: granted, denied or, when missing, unknown. Missing consent never counts as granted unless the property's settings say so.

Property consent settings

settings.consent is set at creation or with PATCH /v1/properties/:id:

SettingDefaultEffect
requireForAnalyticstrueAnalytics events need analytics consent. When false, they are accepted unless analytics is explicitly denied.
requireForMarketingtrueThe same for marketing events.
defaultWhenUnknowndeniedWhat unknown counts as when consent is required (granted or denied).
honorGpctrueGlobal Privacy Control denies marketing.

privacy_mode: "strict" on the property forces both requirements on and defaultWhenUnknown to denied, whatever the settings say.

Settings are merged one level deep: when you change consent, send the whole consent object. Keys you leave out fall back to their defaults, not to their previous values.

curl -sS -X PATCH https://apis.kentro.live/pixel/v1/properties/$PROPERTY_ID \
  -H "Authorization: Bearer $KENTRO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "settings": { "consent": { "requireForAnalytics": true, "requireForMarketing": true, "defaultWhenUnknown": "denied", "honorGpc": true }, "sensitivePathPatterns": ["/account", "/patients"] } }'

How the decision comes out:

Event purposeState in the snapshotResult
analytics or marketinggrantedaccepted
analytics or marketingdeniedsuppressed
analytics or marketingunknowndefaultWhenUnknown when the purpose is required (suppressed by default); accepted when it is not
marketingany, with GPC and honorGpcsuppressed
functionaldeniedsuppressed
functionalgranted or unknownaccepted

An unknown marketing state is never treated as granted: with requireForMarketing: false or defaultWhenUnknown: "granted" such an event is accepted, but anything that needs marketing consent later (ad networks, marketing audiences and exports) still requires an explicit grant in the person's consent record.

What suppression means

A suppressed event is recorded with processing_status: suppressed_consent so that consent coverage can be measured (ingestion health and analytics report suppression counts). It is stored minimized: the event name, timestamps, the consent snapshot and a coarse context (referrer domain, device class, bot flag and locale) only, without properties, identifiers, references, page URL or page title. It never enters sessions, conversions, attribution, audiences or destination deliveries, and it does not use up its event_id.

One exception keeps consent choices from getting lost: when the snapshot states a choice (a purpose explicitly granted or denied, or an active GPC signal), the suppressed event also keeps the person keys the consent record is filed under (the anonymous id, and a verified or server-sent subject_ref or commerce_subject_ref). Kentro records the stated choices in the consent record, then removes those keys from the stored event.

Consent record

Kentro keeps a consent record per property for each person key: the anonymous id (subject type anonymous), the signed-in subject reference (authenticated) and the commerce customer reference (commerce_customer). It is built from the snapshots on incoming events, and audiences and destination deliveries check it at the time of use, on top of the decision taken at ingestion.

  • Only explicit choices are recorded: a purpose the snapshot leaves unknown is not a choice, so a property default never becomes a decision.
  • Choices are ordered by the time Kentro received them; the recorded_at your banner sends is kept as evidence only. The latest explicit statement of each purpose decides, and a GPC signal stays in force until a snapshot says gpc: false.
  • A consent_withdrawal privacy request records every purpose as denied. A later snapshot overrides it only when its choice was recorded after the withdrawal was received (and is not dated more than 5 minutes ahead of its arrival).
  • At use time, marketing needs an explicit grant (and no GPC signal where the property honors GPC); ad networks need an explicit marketing grant and no GPC signal at all; analytics follows the explicit state, else the property's rule; functional is allowed unless explicitly denied.

Sensitive-data quarantine

Before an event is stored, Kentro scans the string values of properties, the page title, the page path, UTM values, references and the form field values of lead events with four detectors:

DetectorMatches
health_termA maintained health vocabulary in three tiers, matched as whole words in any case (plurals included). Named conditions, medications, treatments and drug classes (for example diabetes, ozempic, chemotherapy) count in every string. Clinical words (for example diagnosis, prescription, treatment, patient, appointment, lab result) count in free text: page titles, URL paths, UTM values, references, search queries, reasons, notes and lead fields. Generic health words (for example medical, pharmacy, therapy, weight loss) count only on health-vertical properties.
icd10_codeICD-10 style codes with a decimal part, such as E11.9 or J45.909.
secretsk_live_ keys, AWS access key ids (AKIA...), PEM private keys or certificates and JWT-like tokens.
card_numberRuns of 13 to 19 digits (spaces or dashes allowed) that pass the Luhn check, except under id-like keys (such as order_id, sku or invoice_number) and pure-digit ids from server sources.

A hit quarantines the event: the result status is quarantined with SENSITIVE_PAYLOAD, properties is replaced by { "quarantined": true }, the page title by [redacted], a matching page path is redacted and a matching UTM value or reference removed, the row is classified sensitive, only the detector names are kept, and the event is never processed. A payload too large or too deeply nested to scan completely (more than 2,000 strings or 12 levels) is quarantined as SENSITIVE_SCAN_INCOMPLETE. Authoritative commerce events from your server are not quarantined: the matching fields are replaced by [redacted] (id-like fields by a stable redacted_ value), the order still counts as revenue, the result carries SENSITIVE_REDACTED, and the event is never forwarded to destinations.

Because page titles are scanned as free text, a page titled "Book an appointment" quarantines every page view on it. settings.privacy.healthVertical (true, false or null; null, the default, means on under strict privacy mode) widens the scan for health businesses: clinical words then count in every string, and the generic health words count too.

Sensitive path patterns

settings.sensitivePathPatterns lists the URL paths that carry personal or sensitive data, for example account, order or patient pages. Matching paths are redacted wherever they appear: in page_url, page_path and every URL- or path-valued property.

PatternMatches
/accountThe path /account and everything under it (/account/orders/1001).
/patient/*/labs* as a whole segment matches exactly one segment: /patient/123/labs.
/order-** inside a segment matches any run of characters there: /order-1001.
**/reset-passwordA leading **/ matches at any depth: /en/account/reset-password.
  • Patterns start with / (or **/) and are at most 200 characters and 20 segments; a property can have up to 50. Matching is case-insensitive and also tries percent-decoded segments.
  • Redaction starts at the last segment of the matched part: /account/orders/1001 becomes /[redacted]/[redacted]/[redacted], and /patient/123/labs/today becomes /patient/123/[redacted]/[redacted].
  • Regular expressions are not accepted. A simple legacy expression such as ^/account is converted when it is saved; anything else is refused with 400. A stored value that cannot be converted redacts every path (/*) until it is fixed.

Set them in the property's Settings tab in Kentro GO (Sensitive paths) or with PATCH /v1/properties/:id.

Form fields and lead details

Lead events (marketing.lead_captured and marketing.form_submitted) may carry properties.fields as an object of field values. Whatever the settings, the event record keeps only the field names (for form_submitted) and the hashed customer.email_sha256 and customer.phone_sha256; the values are scanned by the detectors and then discarded. Only when the property's settings.lead.rawEmail is true (default false; true for landing pages created by Kentro GO) and the event was accepted, the trimmed and format-checked email, name and phone are added to the person's Pixel record (the newest 20 submissions are kept), where a privacy deletion removes them.

Privacy requests

Privacy requests cover one person across every identifier Kentro Pixel knows. All four routes need pixel:manage, because subject keys identify people. In Kentro GO, the Pixel page's Privacy tab files and follows them too.

curl -sS https://apis.kentro.live/pixel/v1/privacy/requests \
  -H "Authorization: Bearer $KENTRO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "delete",
    "subjectKeys": [
      { "type": "email_sha256", "key": "[email protected]" },
      { "type": "anonymous_id", "key": "anon_6f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e" }
    ]
  }'
FieldValues
typeexport (a copy of what Kentro Pixel holds for the subject keys), delete (remove the person's Pixel records), anonymize (remove the identifying data from the person's records) or consent_withdrawal (record that the person withdrew consent).
subjectKeys1 to 100 entries of { "type", "key" }. Types: anonymous_id, subject_ref, commerce_subject_ref, email_sha256, phone_sha256.

email_sha256 and phone_sha256 keys accept either a SHA-256 hex digest or a raw email address or phone number; raw values are hashed on arrival with the collector's normalisation (email trimmed and lower-cased; phone reduced to 7 to 15 digits) and discarded, so they never reach storage, logs or error messages. Other keys are 1 to 256 characters. Duplicates are removed. An unusable key answers 400 INVALID_SUBJECT_KEY.

{
  "id": "0192f0c9-4d5e-7f60-8172-8c9d0e1f2a3b",
  "type": "delete",
  "subjectKeys": [
    { "type": "email_sha256", "key": "8c87b489ce35cf2e2f39f80e282cb2e804932a56a213983eeeb428407d43b52d" },
    { "type": "anonymous_id", "key": "anon_6f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e" }
  ],
  "status": "received",
  "requestedBy": "7d1f2e3a-4b5c-4d6e-8f90-1a2b3c4d5e6f",
  "verification": { "requestedVia": "pixel-api", "requestedBy": "7d1f2e3a-4b5c-4d6e-8f90-1a2b3c4d5e6f", "requestedAt": "2026-10-09T18:00:00.000Z", "subjectKeyCount": 2, "hashedFromRaw": 1 },
  "result": null,
  "resumable": false,
  "createdAt": "2026-10-09T18:00:00.000Z",
  "completedAt": null,
  "jobId": "0192f0c9-4d5f-7a71-9283-9d0e1f2a3b4c"
}

Requests are processed asynchronously. Follow them with GET /v1/privacy/requests/:id or list them with GET /v1/privacy/requests?status&type&limit&cursor ({ "items": [...], "nextCursor": ... }). Statuses are received, verifying, processing, completed and failed.

What processing does:

TypeResult
exportA JSON export of the person's Pixel data; result.url is a download link valid for 24 hours (result.expiresAt; a very large export comes in result.parts).
deleteThe person's Pixel records are deleted: their events are removed from the event history; their sessions, touchpoints and consent history are removed; their conversions are unlinked from them; audience memberships are ended (the next webhook sync reports the leave as deleted) and queued deliveries are cancelled.
anonymizeIdentifying data is erased from the person's events (identifiers, raw click ids and user agent, page context and properties), but the sessions, touchpoints and conversions that feed reports are kept under a new pseudonymous key.
consent_withdrawalEvery purpose is recorded as denied in the consent record, queued deliveries are cancelled and audience memberships are ended.
  • A request covers exactly one person. A device the person shared with someone else (an anonymous id linked to another person) is not expanded through: it is listed in result.sharedDevices for your review, and only the requester's own records on it are touched.
  • Hashed email and phone keys also match retained lead records.
  • A very large request stops after 50,000 events in one run and is recorded failed with result.partial: true and "resumable": true; Kentro retries it automatically from where it stopped. If it is still failed after the automatic retries, POST /v1/privacy/requests/:id/resume queues it again (202, "resumed": true). That route answers 409 NOT_RESUMABLE for a request that is completed, failed for good or still processing, and 409 PRIVACY_JOB_ACTIVE while a run of the request is queued or running.

Limits with external providers

Kentro cannot recall data that a destination already received. When a request is completed, result.destinations lists every destination that is enabled or has delivered before, each with irreversible: true and what to do:

  • Meta Conversions API: events already delivered cannot be recalled by Kentro; request deletion through Meta Business tools (a data deletion request for the dataset).
  • Google Ads offline conversions: uploaded conversions cannot be deleted by Kentro, and a retraction only adjusts reporting; use the Google Ads account data controls.
  • GA4 Measurement Protocol: events already sent can only be removed with GA4 user deletion (Admin or the User Deletion API).
  • Webhook: payloads already delivered are under the receiving system; notify its owner.

A privacy request does not touch the person's browser. An explicit denial through consentChanged deletes what the SDK stored on that device, and reset forgets everything the SDK stored.

Destinations

Destinations forward conversion events (and, for webhooks, audience membership) to the systems you run your marketing in. Every destination starts in a safe state, can be tested with a synthetic event, and exports nothing until you enable it.

Mapped events of an enabled destination are delivered in the background. Each delivery is retried with exponential backoff (2^attempts × 30 seconds, at most 1 hour; a provider's Retry-After is honored instead) for up to 8 attempts, and every outcome is listed in the destination's delivery history.

Consent is checked again when a delivery is sent: an ad network (Meta Conversions API, Google Ads) receives a person only with an explicit marketing grant and no GPC signal in their consent record, whatever consentPurpose the destination stores; properties in strict privacy mode never send to ad networks; quarantined events and redacted (sensitive) server events are never sent.

Destination types

destinationconfig (stored and returned)credentials (write-only, encrypted at rest)
meta_capipixelId (5 to 32 digits), optional apiVersion such as v26.0, optional testEventCode (the Events Manager test code: up to 64 letters, digits, _ or -)accessToken (10 to 2,048 characters)
google_ads_offlinecustomerId (123-456-7890 or 10 digits), conversionAction (customers/<id>/conversionActions/<id>), optional loginCustomerId, credentialSource: "go_connection", goConnectionIdnone: Kentro uses your Kentro GO Google Ads connection
ga4_mpmeasurementId (G- plus 4 to 20 upper-case letters or digits)apiSecret (8 to 256 characters)
webhookurl (https:// with a public host name, up to 2,048 characters)signingSecret (16 to 256 characters)

Credentials are never returned; every destination reports hasCredentials and credentialSource (stored, go_connection or null) instead, plus testMode, deliveryMode (test or live) and, in test mode, a testModeNote saying what the provider does with test deliveries. In Kentro GO, the Pixel page's Destinations tab creates and edits all four types.

Create a destination

curl -sS https://apis.kentro.live/pixel/v1/destinations \
  -H "Authorization: Bearer $KENTRO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "destination": "meta_capi",
    "name": "Meta dataset",
    "propertyId": "0192f0c4-6d1e-7a52-9c1f-3b1f2c7f9a10",
    "config": { "pixelId": "123456789012345", "testEventCode": "TEST12345" },
    "credentials": { "accessToken": "YOUR_META_ACCESS_TOKEN" },
    "eventMapping": [
      { "source": "commerce.order_paid", "target": "Purchase" },
      { "source": "marketing.lead_captured", "target": "Lead" }
    ],
    "consentPurpose": "marketing"
  }'
{
  "id": "0192f0ca-5e6f-7081-9293-a4b5c6d7e8f9",
  "propertyId": "0192f0c4-6d1e-7a52-9c1f-3b1f2c7f9a10",
  "destination": "meta_capi",
  "name": "Meta dataset",
  "status": "configured",
  "testMode": true,
  "deliveryMode": "test",
  "testModeNote": "Test mode: every delivery is sent with config.testEventCode and shows only in Events Manager > Test Events; nothing reaches the live dataset.",
  "config": { "pixelId": "123456789012345", "testEventCode": "TEST12345" },
  "eventMapping": [
    { "source": "commerce.order_paid", "target": "Purchase", "fields": {}, "enabled": true },
    { "source": "marketing.lead_captured", "target": "Lead", "fields": {}, "enabled": true }
  ],
  "consentPurpose": "marketing",
  "mappingVersion": 1,
  "lastDeliveryAt": null,
  "error": null,
  "hasCredentials": true,
  "credentialSource": "stored",
  "createdBy": "7d1f2e3a-4b5c-4d6e-8f90-1a2b3c4d5e6f",
  "createdAt": "2026-10-09T18:10:00.000Z",
  "updatedAt": "2026-10-09T18:10:00.000Z"
}
  • propertyId is optional; when set it must belong to your workspace (400 PROPERTY_NOT_FOUND).
  • consentPurpose (default marketing) is the purpose a person must have granted before anything about them is exported to this destination.
  • testMode is ignored on creation: every destination starts with status: "configured" and testMode: true.

Event mapping

eventMapping is a list of up to 50 entries { "source", "target", "enabled" }, each with an optional fields object of up to 50 string pairs (webhooks use it to fill data, see "Webhooks"). source must be a builtin or an active custom event of your workspace; duplicates of the same source and target are refused; enabled defaults to true. target depends on the destination:

DestinationValid targets
meta_capiA Meta standard event (Purchase, Lead, CompleteRegistration, AddToCart, InitiateCheckout, ViewContent, Search, Subscribe, Contact, Schedule) or a custom event name: a letter, then letters, digits or underscores, at most 50 characters.
ga4_mpA Measurement Protocol event name: a letter, then letters, digits or underscores, at most 40 characters, not a reserved GA4 name, app-only name or reserved prefix.
google_ads_offlineA conversion action resource name, customers/<id>/conversionActions/<id>.
webhook1 to 100 letters, digits, dots, colons, dashes or underscores.

Invalid mappings answer 400 EVENT_MAPPING_INVALID with an errors array. Changing the mapping with PATCH increments mappingVersion.

Lifecycle and test mode

StatusMeaning
configuredCreated, exports nothing. Test sends are allowed.
enabledExports mapped events.
disabledSwitched off by you.
errorSet by Kentro when delivery fails; not settable through the API.

Enable with PATCH /v1/destinations/:id and { "status": "enabled" }. Enabling requires stored credentials (or a linked Kentro GO connection), otherwise 409 CREDENTIALS_REQUIRED, and at least one enabled mapping, otherwise 409 EVENT_MAPPING_REQUIRED. A meta_capi destination in test mode also needs config.testEventCode, otherwise 400 TEST_EVENT_CODE_REQUIRED. PATCH also updates name, propertyId, config, credentials (null clears the stored secret), eventMapping, consentPurpose and testMode. DELETE /v1/destinations/:id removes the destination and keeps its delivery history.

testMode is a flag you control: new destinations start with testMode: true, and PATCH with { "testMode": false } turns it off. While it is on, an enabled destination delivers only as tests: Meta deliveries carry config.testEventCode and appear only in Events Manager's Test Events, GA4 deliveries go to the validation endpoint and Google Ads uploads run with validateOnly (both record nothing), and webhook requests carry "test": true and an X-Kentro-Test: 1 header.

Test sends

POST /v1/destinations/:id/test sends one synthetic event through the real provider API and records the outcome as a delivery row. The synthetic identity is a hashed constant, never a real person.

DestinationWhat the test does
meta_capiSends one event (the first enabled mapping's target, else Lead) to the Conversions API with the testEventCode from Events Manager, Test Events, so it shows only there. Without a body of { "testEventCode": "TEST12345" } the test is refused with 400 TEST_EVENT_CODE_REQUIRED.
ga4_mpSends one event to the Measurement Protocol validation endpoint, which validates without recording anything, and returns its validation messages. The delivery row is sent with validateOnly: true (or rejected with the messages).
google_ads_offlineUploads one click conversion with validateOnly: true (Google validates and records nothing) using an access token from your Kentro GO Google Ads connection. The delivery row is sent with validateOnly: true (or rejected).
webhookPOSTs a signed synthetic payload (kentro.pixel.destination_test) to your URL with a 5 second timeout; any 2xx answer counts as accepted.
curl -sS https://apis.kentro.live/pixel/v1/destinations/$DESTINATION_ID/test \
  -H "Authorization: Bearer $KENTRO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "testEventCode": "TEST12345" }'
{
  "ok": true,
  "status": "accepted",
  "validateOnly": false,
  "deliveryId": "0192f0cb-6f70-7192-a3b4-c5d6e7f8a9b0",
  "destination": "meta_capi",
  "testMode": true,
  "deliveryMode": "test",
  "providerResponse": { "eventsReceived": 1, "messages": [], "fbtraceId": "A1b2C3d4E5f6" },
  "error": null,
  "note": "Provider acceptance means a well-formed test request was accepted. It is not a matched or attributed conversion."
}

A test that cannot be attempted answers an error instead of a delivery row: 409 CREDENTIALS_REQUIRED (no stored secret), 400 TEST_EVENT_CODE_REQUIRED (Meta), 409 GO_CONNECTION_NOT_FOUND (the Kentro GO connection does not exist or is disconnected), 409 GO_CONNECTION_NOT_USABLE (the connection cannot issue an access token: reconnect the provider in Kentro GO), 502 GO_PROVIDER_UNAVAILABLE (Google could not refresh the access token right now), 502 GO_API_ERROR, 503 GO_API_UNAVAILABLE (Google Ads sending through Kentro GO is not configured on this Kentro environment), 422 WEBHOOK_TARGET_NOT_ALLOWED (the URL fails the rules below) and 503 CREDENTIALS_UNAVAILABLE (stored credentials cannot be opened).

Google Ads offline conversions

Google Ads destinations authenticate through a Kentro GO Google Ads connection: set credentialSource: "go_connection" and the connection's id in goConnectionId (connections are listed by GET https://apis.kentro.live/go/v1/connections). You do not need a Google Ads developer token: Google sunset developer tokens on 2026-09-09, and Kentro sends conversions with the OAuth access token of your connection. Kentro never stores Google tokens on the destination. conversionAction is the resource name of an upload-type conversion action in your Google Ads account; loginCustomerId is the manager account id when the connection signs in through one (it takes precedence over the manager id configured for Kentro GO). Mapped conversions are uploaded as click conversions and need a Google click id (gclid, gbraid or wbraid) captured on a visit with marketing consent; without one the delivery is suppressed (no_click_id). Refunds and cancellations are sent as adjustments: a retraction for a cancellation or a full refund, a restatement of the remaining value for a partial refund. The consent signals on each upload (adUserData, adPersonalization) come from the person's consent record.

Webhooks

Webhook URLs must use https:// and a public host name: no IP literals, localhost, .local or .internal names, no credentials and no fragment in the URL. At send time every address the host resolves to must be public and redirects are not followed. Live deliveries time out after 10 seconds (test sends after 5). A 2xx answer counts as accepted; 408, 429 and 5xx answers and network errors are retried; any other answer is recorded as rejected. Kentro reads at most the first 4 KB of your response.

Every request Kentro sends to a webhook carries these headers:

HeaderValue
Content-Typeapplication/json
User-AgentKentro-Pixel-Webhook/1.0
X-Kentro-EventThe payload type: kentro.pixel.event, kentro.pixel.audience_membership or kentro.pixel.destination_test
X-Kentro-DeliveryLive deliveries: the delivery id, the same on every retry (use it to drop repeats)
X-Kentro-TestLive deliveries of a destination in test mode: 1
X-Kentro-TimestampUnix time in seconds
X-Kentro-Signaturesha256= followed by the hex HMAC-SHA256 of timestamp + "." + rawBody, keyed with your signingSecret

Payload types:

  • kentro.pixel.event (one mapped event): type, id (the delivery id), created_at, destination_id, test, targets (the mapping targets the event matched), event and, when the matched mapping entry has fields, data. event carries event_id, event_name, occurred_at, received_at, source_type, authoritative, refs (the order, campaign, creative, landing page and experiment references, subject_ref, commerce_subject_ref, anonymous_id and session_id), context (page_path, referrer_domain, utm, device_class), the effective consent and properties. data holds one value per fields entry, read from a dotted path of event: { "order": "refs.order_ref" } sends "data": { "order": "<the order ref>" }.
  • kentro.pixel.audience_membership (one page of an audience sync): type, id, created_at, destination_id, test, audience (id, name), mode, window (from, to), page, page_size, and joined and left lists of { "subject_id", "subject_type", "subject_key" }; a leave caused by a privacy deletion carries "deleted": true.
  • kentro.pixel.destination_test (a test send).

The test payload looks like this:

{
  "type": "kentro.pixel.destination_test",
  "id": "0192f0cb-6f70-7192-a3b4-c5d6e7f8a9b0",
  "created_at": "2026-10-09T18:20:00.000Z",
  "destination_id": "0192f0ca-5e6f-7081-9293-a4b5c6d7e8f9",
  "test": true,
  "event": { "event_name": "kentro.test", "event_id": "kentro_test_0192f0cb-6f70-7192-a3b4-c5d6e7f8a9b0", "occurred_at": "2026-10-09T18:20:00.000Z", "properties": { "synthetic": true } }
}

Verify the signature over the raw request bytes before parsing the JSON, and reject timestamps outside a window you choose (Kentro uses 300 seconds for its own signatures):

const crypto = require('node:crypto');
const http = require('node:http');

const SIGNING_SECRET = process.env.KENTRO_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;

function verifyKentroSignature(rawBody, headers, secret) {
  const timestamp = headers['x-kentro-timestamp'];
  const signature = headers['x-kentro-signature'] || '';
  if (!/^\d+$/.test(timestamp || '')) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

http.createServer((req, res) => {
  const chunks = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('end', () => {
    const rawBody = Buffer.concat(chunks);
    if (!verifyKentroSignature(rawBody, req.headers, SIGNING_SECRET)) {
      res.writeHead(401).end();
      return;
    }
    const payload = JSON.parse(rawBody.toString('utf8'));
    console.log('Kentro webhook', req.headers['x-kentro-event'], payload.id);
    res.writeHead(204).end();
  });
}).listen(8080);

Only webhook destinations can receive audience membership; Meta, Google Ads and GA4 destinations receive conversion events only, and an audience sync aimed at one of them is refused with 409 DESTINATION_CAPABILITY.

Deliveries and what "accepted" means

GET /v1/destinations/:id/deliveries?status&limit&cursor lists delivery rows, newest first, as { "items": [...], "nextCursor": ... }. Each row carries id, destinationId, journalId, conversionId, dedupeKey, status, testMode (a test send, or a delivery of a destination in test mode), validateOnly (the provider only validated the request), attempts, requestRedacted (the request without secrets), response (the provider's answer, redacted), providerReceipt, suppressedReason, sentAt, nextAttemptAt, error and createdAt.

StatusMeaning
queuedWaiting to be sent.
sentThe request left Kentro, but the provider does not confirm acceptance: GA4 (its collection endpoint answers without validating) and validate-only test calls (GA4 validation, Google Ads validateOnly), which record nothing.
acceptedThe provider accepted the request.
rejectedThe provider refused it; see error and response.
failedKentro could not complete the delivery; see error.
suppressedKentro deliberately did not send it; suppressedReason says why: consent_not_granted, gpc_opt_out, strict_privacy_mode, quarantined, sensitive_event, no_click_id (Google Ads: no consented Google click id), no_client_id (GA4: no client id), privacy_request or consent_withdrawn.

accepted means the provider accepted a well-formed request. It does not mean the provider matched the event to a person, counted a conversion or attributed it to a campaign; test events never appear in live reports at all (Meta shows them under Test Events, GA4 and Google Ads validate without recording).

POST /v1/destinations/:id/sync with an optional { "from", "to" } window (default the last 7 days, at most 180 days long and starting at most 30 days back; YYYY-MM-DD values are whole UTC days with an inclusive end) queues a replay of the window's events for an enabled destination and answers 202 with a jobId; a destination that is not enabled answers 409 DESTINATION_NOT_ENABLED. Replay recognises the events it already delivered by their delivery history, which is kept for 30 days, so a window whose from is older than that answers 400 REPLAY_RANGE_TOO_OLD.

Legacy /track compatibility

The original pixel routes keep working unchanged on the same host, byte for byte:

  • GET https://pixel.kentro.live/track?event=<name>&... answers a 200 transparent GIF (43 bytes, Cache-Control: no-cache, no-store, must-revalidate, Access-Control-Allow-Origin: *) whatever the query contains. The query string is the event; event defaults to page_view.
  • POST /track (JSON) and POST /track-flexible (JSON, a JSON string or a form-encoded body) answer 200 { "status": "tracked", "tracking_id": "...", "event": "..." }, or 400 { "status": "error", "message": "Invalid event type: ..." } for a name outside the legacy list. Bodies up to 1 MB.
  • The legacy names are page_view, product_view, add_to_cart, remove_from_cart, view_cart, begin_checkout, checkout_step, purchase, category_view, search_query, filter_usage, product_click, promotion_view, promotion_click, email_subscription, wishlist_addition, promo_code_application, checkout_abandonment, subscription_create, subscription_cancel, subscription_pushed, account_creation, login, subscription_action and init.

Adopting a legacy snippet into a property

Add key=<public key> to a legacy call (in the query string, or in the body for POST) and Kentro also records the event for that property as a v1 event, after the legacy response was sent:

<img src="https://pixel.kentro.live/track?event=page_view&key=pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze&page_url=https%3A%2F%2Fwww.example.com%2Fpricing" width="1" height="1" alt="">
curl -sS https://pixel.kentro.live/track \
  -H "Content-Type: application/json" \
  -H "Origin: https://www.example.com" \
  -d '{ "event": "add_to_cart", "key": "pk_live_Xq3M9b2LkV7tR1sNw8YhC4Ze", "product_id": "sku-1", "product_name": "Mug", "price": 19.99, "quantity": 1, "currency": "USD" }'

What adoption does:

  • The legacy name is mapped to the taxonomy (table below); names without a dedicated mapping, and unknown names on GET, become marketing.content_viewed. The original name is kept in properties.legacy_event.
  • Scalar fields of the payload (up to 50; strings cut to 512 characters) become properties, except event, key and personal fields, which are dropped: names, postal addresses, birth dates, government ids, credentials, card data, IP addresses and user agents, and the identifier fields anonymous_id, visitor_id, client_id, session_id, customer_id and user_id.
  • page_url or url (else the Referer header) becomes the page URL, page_title or title the page title, referrer or referer the referrer, anonymous_id, client_id or visitor_id the anonymous id, session_id the session id and order_id the order_ref. A legacy call never sets a person reference: customer_id is not used as commerce_subject_ref.
  • The event is recorded with source_type: legacy, skips the strict property schema (legacy payloads are not v1-shaped) and is never authoritative. The origin allowlist, rate limits, sensitive-data detectors and consent decision apply as for any browser event.
  • The legacy response never changes; adoption failures are logged, not reported. Check the event debugger and ingestion health.

Two caveats:

  • Consent. Legacy calls carry no consent snapshot, so consent is unknown. With the default settings (defaultWhenUnknown: "denied"), adopted analytics and marketing events are recorded as suppressed; functional ones such as purchase (commerce.order_placed) are accepted. Migrating to the SDK, which sends real consent states, is the way to get the rest accepted.
  • Personal data. Fields named like contact fields (email, customer_email, phone_number, ...) are hashed, email addresses inside other values are masked, and the personal fields listed above are dropped. Any other field is stored as sent, so keep other personal data out of legacy payloads before adding a key.
Legacy nameRecorded as
page_viewmarketing.page_viewed
product_viewcommerce.product_viewed
add_to_cartcommerce.added_to_cart
remove_from_cartcommerce.removed_from_cart
begin_checkoutcommerce.checkout_started
checkout_stepcommerce.checkout_step_completed
purchasecommerce.order_placed
category_viewcommerce.collection_viewed
search_querymarketing.search_performed
product_click, promotion_clickmarketing.cta_clicked
email_subscriptionmarketing.email_opt_in_recorded
wishlist_additionmarketing.asset_engaged
subscription_createcommerce.subscription_started
subscription_cancelcommerce.subscription_cancelled
account_creationmarketing.signup_completed
initmarketing.session_started
view_cart, filter_usage, promotion_view, promo_code_application, checkout_abandonment, subscription_pushed, login, subscription_action and unknown namesmarketing.content_viewed

Processing and retention

After ingestion, Kentro's background processor works through accepted events in the order they arrived:

  • People and sessions. Each event is attached to the person keys it carries (anonymous id, subject_ref, commerce_subject_ref); a verified login or an order links an anonymous visitor to the person. Sessions follow the property's settings.session (30 idle minutes by default) and record the touchpoint: UTM values, ad click ids (hashed), the campaign reference and the referrer class. Sessions, touchpoints and browser conversions need analytics or marketing consent.
  • Consent record. Explicit consent choices are filed as described in "Consent record".
  • Conversions. Lead, signup, demo, checkout, order and subscription events become conversion facts, and refunds and cancellations correct them. Analytics count each order once (commerce.order_paid wins over commerce.order_placed for the same order_ref), remove cancelled orders and subtract refunds.
  • Destinations. Each mapped event of an enabled destination becomes a delivery, sent with retries; consent is checked again when it is sent.
  • Attribution. Last-touch attribution over 30 days is refreshed automatically when conversions change; the other models run on request (POST /v1/analytics/attribution/runs).
  • Audiences. Active audiences are recomputed about every 10 minutes; the membership changes are what an audience sync sends.
  • Privacy requests run as described in "Privacy requests".

Retention is enforced automatically: each kind of data expires after the period below, without any action on your side. Changing settings.retention applies to the history already stored, not only to new events: a longer period keeps the existing history longer, and a shorter one removes the stored history past the new period, starting with the next daily retention run.

DataKept
Event historyExpires automatically after the retention period: at least 13 months, or longer when settings.retention.journalMonths asks for it
Ingestion receipts and server batch idempotency keys (X-Kentro-Idempotency-Key)As long as the property's event history: at least 13 months, or longer when settings.retention.journalMonths asks for it
Accepted event ids, for duplicate detection14 days
Rejected-event samples (ingestion health)settings.retention.errorsDays: 30 days by default, 1 to 365
Attribution results and order reconciliation recordsAs long as the property's event history
People records (the person keys an event carries and the links between them, such as a verified login or an order linking an anonymous visitor to a customer)Removed once a person has not been seen for the full event retention period, belongs to no active audience and has no conversion within that period. The deletion records of privacy requests are kept, so a deleted person is never recreated
Consent recordsKept as long as the person's record is kept (see People records above), and at least for the event retention period; once the person's record has been removed, their consent records older than the event retention period are removed too, unless a newer consent choice was recorded within that period. Consent withdrawal records are always kept
Audience membership historyA person's current memberships are kept while they last; the record of a person leaving an audience is kept 90 days
Raw ad click ids90 days, then removed from sessions and events
The full browser user agent of marketing-consented events30 days
The salted hash of the client IP address kept on ingestion receipts (the raw address is never stored)30 days
Delivery history (GET /v1/destinations/:id/deliveries)30 days; a delivery still waiting to be sent is kept until it is sent or gives up
Privacy request records24 months after completion, as the CCPA regulations require, then deleted
Privacy export downloads24 hours

Limits and errors

Limits

LimitValue
Browser request body (/v1/events, /v1/events/batch)64 KB
Server request body (/v1/server/events)512 KB
One event, serialized32 KB
Events per request100 on browser routes and the validate endpoint, 500 on the server route
GIF beacon e parameter16 KB of base64url
Legacy request body1 MB
occurred_at skew7 days in the past, 5 minutes in the future
Server signature window300 seconds
Idempotency key128 characters
Rate limit, public key600 events per minute, bursts up to 1,200
Rate limit, server key6,000 events per minute, bursts up to 12,000
Rate limit, client IP on browser routes and the validate endpoint600 requests per minute, bursts up to 1,200
Rate limit, client IP on the server route1,200 requests per minute, bursts up to 2,400
Lookups of keys not seen in the last minute, per client IP60 per minute, bursts up to 120
Identity tokens5 distinct tokens per request, 1,024 characters each, valid at most 24 hours
Duplicate detection (event_id)At least 14 days per property
Allowed origins50 per property
Sensitive path patterns50 per property, 200 characters and 20 segments each
Custom event schemas500 per workspace are loaded
Destination event mapping50 entries
Destination replay window (POST /v1/destinations/:id/sync)At most 180 days long, from at most 30 days back (400 REPLAY_RANGE_TOO_OLD)
Privacy request subject keys1 to 100

Key rate limits count events, not requests (a batch of 50 uses 50); client IP limits count requests, and lookups of keys the collector has not seen in the last minute count separately, so unknown keys cannot be used to scan for valid ones. All limits are enforced per collector instance. A refused request answers 429 RATE_LIMITED with a Retry-After header in seconds; the SDK retries automatically.

CORS

The collector endpoints answer CORS themselves: Access-Control-Allow-Origin echoes the request origin when the property allows it (or *), with Vary: Origin, methods POST, GET, OPTIONS, headers Content-Type, X-Kentro-Pixel-Key and a preflight cache of 10 minutes. Credentials are not used. The SDK sends text/plain with the key in the body, which needs no preflight; sending application/json or the X-Kentro-Pixel-Key header from a browser triggers a preflight, which is supported. The SDK file is served with Access-Control-Allow-Origin: * and Cross-Origin-Resource-Policy: cross-origin; the legacy routes answer Access-Control-Allow-Origin: *. The management API uses the standard Kentro API CORS policy.

Troubleshooting

SymptomLikely causeWhat to do
No request leaves the pageThe script tag has no data-key, or init was called without key and endpoint; or a call was queued before init.Use the snippet from GET /v1/properties/:id/snippet, or pass both options to init, and queue init first.
Requests blocked by the browserA Content-Security-Policy without the collector host.Allow https://pixel.kentro.live in script-src and connect-src (and img-src for the GIF fallback), and allow the inline queue stub with a nonce or hash.
401 KEY_INVALIDWrong key, disabled property, or a pk_test_ key on a production property (or the reverse).Copy the key from the property; keys are tied to the property's environment.
401 KEY_REVOKEDThe key was revoked.Create a new key and update the snippet.
403 ORIGIN_NOT_ALLOWEDThe page's host is not in allowed_origins, or the request carried neither Origin nor Referer while an allowlist is set.Add the exact host or a *.domain entry (the apex needs its own entry); a Referrer-Policy: no-referrer page cannot use the GIF beacon with an allowlist.
Result rejected, UNKNOWN_EVENTThe name is not in the taxonomy, is not lower-case namespace.name, or schema_version does not match.Use a name from the taxonomy table or a registered custom schema.
Result rejected, SCHEMA_VIOLATIONA property is not on the event's allowlist, a required property is missing, or money was sent as a float.Check reason; send money as { "amount_minor", "currency" }; put identifiers under properties.customer.
Result rejected, CLOCK_SKEWoccurred_at is far off, or was sent in milliseconds.Send ISO 8601 timestamps from a synchronised clock.
Result suppressed, CONSENT_DENIED or GPC_OPT_OUTConsent is unknown or denied for the event's purpose (CONSENT_DENIED), or Global Privacy Control alone blocked a marketing event (GPC_OPT_OUT).Report consent with consentChanged; include consent in server envelopes; review settings.consent.
Result quarantined, SENSITIVE_PAYLOADA property value, the page title or a form value matched a detector, for example "appointment", "treatment" or "patient" in a page title.Keep medical terms, secrets and card numbers out of event data and page titles.
Result duplicateThe event_id was already accepted for the property, or the envelope had no event_id and was identical to an earlier accepted one.Give every real-world event its own event_id; expect duplicate on retries.
429 RATE_LIMITEDMore than the per-key budget.Wait for Retry-After; batch events; use a server key for high-volume backend traffic.
401 SIGNATURE_INVALID on the server routeThe signed string differs from the bytes sent (re-serialized body, pretty-printing, different encoding), the secret belongs to another key id, the sha256= prefix or lower-case hex is missing, the secret was base64-decoded, or the body was not sent as application/json.Sign the exact string you send, with the secret as given, and send Content-Type: application/json.
401 TIMESTAMP_STALEThe clock is off by more than 300 seconds, or the timestamp is in milliseconds.Send Unix seconds from a synchronised clock.
Result accepted with IDENTITY_UNVERIFIEDThe event carried a person reference without a valid identity token (missing, malformed, expired, or signed by a revoked key or a key of another property).Mint tokens on your server as in "Identity tokens for signed-in visitors" and pass them with identify; never put subject_ref in browser events.
409 IDEMPOTENCY_KEY_REUSEDThe idempotency key was already used with a different request body.Use a new key for every new batch; reuse a key only to retry the identical body.
503 COLLECTOR_DISABLED, INGEST_UNAVAILABLE, SIGNING_UNAVAILABLEThe collector is switched off, or storage or secrets are temporarily unavailable.Retry later with the same event_id values.
Events appear in the debugger but not in analytics or a destinationProcessing runs in the background after ingestion; a destination sends only mapped events once it is enabled, and only with the consent it needs.Check processing_status in the debugger (processed once handled), the destination's status and mapping, and its delivery history (suppressedReason).
404 Not Found on every management routeThe management API is switched off on this environment.During the preview it is enabled per environment; contact Kentro support for access.

Hosts and the management API

HostServesAuthentication
https://pixel.kentro.live (production)The collector (/v1/events, /v1/events/batch, /v1/events/pixel.gif, /v1/server/events), the SDK (/sdk/v1/kentro-pixel.js), the legacy /track routes, the public schema listing and the validate endpointProperty public key or HMAC server key
https://pixel.kentro.dev (staging)The same collector, SDK and legacy routes on Kentro's staging environment, for testingProperty public key or HMAC server key
https://apis.kentro.live/pixel (production)The management API under /v1/...Kentro authentication
https://apis.kentro.dev/pixel (staging)The same management API on staging, for testingKentro authentication

The management API accepts the same credentials as the other Kentro APIs on that host: Authorization: Bearer <access token> from Kentro authentication, or a Kentro API key (X-API-Key, or kentro-key with kentro-secret). Your user or key also needs Pixel permissions: pixel:read for reads and pixel:manage for writes (pixel:manage includes reads; admin and pixel:* include both). Missing credentials answer 401 { "error": "Unauthorized" }; a missing permission answers 403 { "error": "Forbidden", "message": "Missing permission: pixel:manage" }.

Conventions: JSON in and out; errors are { "error", "message" } plus the fields named above; request validation failures are 400 Bad Request; lists paginate with limit and an opaque cursor and answer { "items": [...], "nextCursor": ... } (GET /v1/properties answers { "properties": [...] }). Property routes use snake_case fields (allowed_origins, privacy_mode, public_key); the analytics, audience, destination and privacy routes use camelCase (propertyId, consentPurpose, subjectKeys). Date ranges accept YYYY-MM-DD (whole UTC days, inclusive end) or ISO timestamps and are capped at 180 days.

RoutePermissionPurpose
GET /v1/properties, POST /v1/propertiesread, manageList or create properties (create returns the first public key).
GET /v1/properties/:id, PATCH /v1/properties/:idread, manageOne property with its domains and keys; update name, origins, settings, privacy mode or status.
POST /v1/properties/:id/domains, DELETE /v1/properties/:id/domains/:domainIdmanageRegister or remove a domain.
POST /v1/properties/:id/verifymanageVerify a domain (dns_txt, http_file or meta_tag).
POST /v1/properties/:id/keys, DELETE /v1/properties/:id/keys/:keyIdmanageCreate or revoke a public key.
POST /v1/properties/:id/server-keys, DELETE /v1/properties/:id/server-keys/:keyIdmanageCreate (secret shown once) or revoke a server key.
GET /v1/properties/:id/snippetreadThe install snippet and SDK URL.
GET /v1/event-schemasKentro authentication, no Pixel permissionBuiltin and your custom event schemas.
POST /v1/event-schemas/validateKentro authentication, no Pixel permissionValidate envelopes without storing them (the collector host takes a property key instead).
GET /v1/events/debug?propertyId&limitreadThe newest recorded events (up to 50).
GET /v1/ingestion/health?propertyIdread24-hour counters, freshness and the last rejections.
GET /v1/analytics/overview?propertyId&from&toreadSessions, visitors, page views, events, outcome counts, authoritative versus observed conversions, top sources and campaigns, consent suppression rate, coverage notes.
POST /v1/analytics/funnelsreadOrdered step counts for 2 to 8 events within a window.
GET /v1/analytics/conversions?from&to&type&limit&cursorreadConversion facts and totals.
GET /v1/analytics/attribution?model&windowDays&from&toreadCredits per channel and campaign from the latest attribution run of that model.
POST /v1/analytics/attribution/runsmanageQueue an attribution run (first_touch, last_touch, linear, position_based).
GET /v1/analytics/journeys?subjectId or sessionIdreadThe accepted events of one person or session, in order.
GET /v1/analytics/data-quality?propertyId&from&toreadAccept rate, duplicate ratio, quarantine count, suppression rate, freshness lag.
GET /v1/audiences, POST /v1/audiences, GET /v1/audiences/:id, PATCH /v1/audiences/:id, DELETE /v1/audiences/:idread, manageRule-based audiences over accepted, consented events.
POST /v1/audiences/:id/estimatereadCount the members of the stored or a draft rule set with the consent check exports apply (count; compiledCount before consent; sampled above 10,000 matches). An event condition that more than 100,000 people satisfy answers 422 AUDIENCE_TOO_LARGE with the rule path and the limit: narrow its within_days, add a where filter or raise count_gte.
POST /v1/audiences/:id/syncmanageQueue a membership sync to webhook destinations.
GET /v1/destinations, POST /v1/destinations, GET /v1/destinations/:id, PATCH /v1/destinations/:id, DELETE /v1/destinations/:idread, manageDestinations, as described above.
POST /v1/destinations/:id/test, POST /v1/destinations/:id/sync, GET /v1/destinations/:id/deliveriesmanage, manage, readTest send, replay, delivery history.
POST /v1/privacy/requests, GET /v1/privacy/requests, GET /v1/privacy/requests/:idmanagePrivacy requests.
POST /v1/privacy/requests/:id/resumemanageQueue a resumable privacy request again.

Routes under /internal/ exist for Kentro's own services and are not reachable from the internet.