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 ishttps://apis.kentro.live/pixel. Kentro's staging environment uses the same routes onhttps://pixel.kentro.devandhttps://apis.kentro.dev/pixelfor 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 assuppressed(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_idper property for at least 14 days, so retries are safe. A suppressed or quarantined event does not use up itsevent_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 ascommerce.order_placedare accepted as signals and flagged non-authoritative. Sendcommerce.order_paidfrom your backend; that is what revenue reporting trusts. - No identity claims from the browser. Browser events cannot set
subject_reforcommerce_subject_refthemselves. 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 withIDENTITY_UNVERIFIED.
Hosts at a glance
| What | Production | Staging (testing) | Authentication |
|---|---|---|---|
| Collector and browser SDK | https://pixel.kentro.live | https://pixel.kentro.dev | Property public key, or HMAC server key |
| Management API | https://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"
}'
| Field | Required | Notes |
|---|---|---|
name | yes | 1 to 200 characters. |
type | yes | website, shopify, woocommerce, go_page, mobile_app or server. |
environment | no | production (default), staging or development. Fixed at creation. Production properties get pk_live_ keys, the others pk_test_ keys. |
allowed_origins | no | Up to 50 host names allowed to send browser events. Empty means any origin. See "Domains and verification". |
settings | no | Consent, 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_mode | no | standard (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.kentroPixelas a queue: calls made before the SDK has loaded are kept inkentroPixel.qand replayed in order once it arrives. - The second tag loads the SDK asynchronously.
data-keyis the property's public key. - When the tag carries
data-keyand nothing has queued aninitcall, the SDK initialises itself: it starts a session, sendsmarketing.session_startedand amarketing.page_viewed, and tracks route changes of single-page apps. - With the default consent settings, events are recorded as
suppresseduntil your consent banner reports analytics consent. Wire upconsentChanged(see "Consent integration") before you judge the install.
4. Verify with the event debugger
Open your site, then check three places:
- In the browser's developer tools, the network tab shows
POST https://pixel.kentro.live/v1/eventsanswering202with one result per event:{ "receipt_id": "...", "results": [{ "event_id": "evt_...", "status": "accepted" }] }. - 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.
- 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 apexexample.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
Originheader, or from theRefererheader when there is noOrigin(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:
| Method | Where | Exact format |
|---|---|---|
dns_txt | A 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_file | https://<domain>/.well-known/kentro-pixel-verification.txt | A line that is exactly <token> or kentro-pixel-verification=<token> |
meta_tag | The 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_aton the domain and, the first time, on the property). It does not add the domain toallowed_originsand does not gate ingestion:allowed_originsalone 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'
});
| Option | Default | Description |
|---|---|---|
key | the tag's data-key | Property public key. |
endpoint | origin of the SDK src | Collector base URL. Required when the script tag has no data-key. |
autoPageviews | true | Send marketing.page_viewed during init. |
spa | true | Send a page view on history-based route changes. |
consent | everything unknown | Initial consent snapshot (see consentChanged). May also carry defaultWhenUnknown. |
consent.defaultWhenUnknown | denied | How 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. |
consentProvider | none | A function returning a consent snapshot (or null). Called once during init; its result wins over consent. |
functionalStorage | false (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'. |
campaignRef | the tag's data-campaign-ref | Sent as campaign_ref on every event. |
pageRef | the tag's data-page-ref | Sent as landing_page_ref on every event. |
experimentRef | the tag's data-experiment-ref | Sent as experiment_ref on every event. |
initruns once; later calls are ignored.- Without both a key and an endpoint the SDK stays inactive, silently.
- When the script tag has
data-keyand noinitcall is queued, the SDK callsinititself with the defaults. - Queue
initbefore any other call. Queued calls replay in order:page,track,flushandexperimentcalls placed beforeinitare dropped, and aninitreplayed afterconsentChangedreplaces 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
initsendsmarketing.session_started(entry_url,referrer_domain,landing) when a new session begins, then onemarketing.page_viewedunlessautoPageviewsisfalse.- With
spa: truethe SDK wrapshistory.pushStateandhistory.replaceStateand listens forpopstate. Whenever the URL differs from the last page view it sends a newmarketing.page_viewed. URL rewrites throughreplaceStatecount too; if your app rewrites the URL often, setspa: falseand callpageyourself. - A session ends after 30 minutes without events. The next event starts a new session id and sends
marketing.session_startedfirst.
Data attributes
| Attribute | Equivalent |
|---|---|
data-key | init({ key }) |
data-campaign-ref | init({ campaignRef }) |
data-page-ref | init({ pageRef }) |
data-experiment-ref | init({ 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
| Name | Where | Contents | Lifetime | Written |
|---|---|---|---|---|
kp_anon | First-party cookie (Path=/, SameSite=Lax, Secure on HTTPS) and a localStorage copy | Random anonymous id (anon_ plus 32 hex characters) | 13 months (390 days) | Only while storage is allowed; deleted on an explicit denial |
kp_sess | localStorage | Random session id and two timestamps | Replaced after 30 idle minutes | Only while storage is allowed |
kp_cmp | sessionStorage | Kentro GO campaign reference taken from utm_id | The browser tab session | Only while storage is allowed |
kp_exp_<experimentRef> | localStorage | Assigned experiment arm | Until an explicit denial or reset clears it | Only 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.globalPrivacyControlasgpc: truein every snapshot. It never changes the purposes your consent manager reported. - At the collector,
consent.gpc: trueor theSec-GPC: 1request header makes marketing consentdeniedfor that event whilesettings.consent.honorGpcistrue(the default). A marketing event that only GPC blocked is recordedsuppressedwithGPC_OPT_OUT; an explicit denial staysCONSENT_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
honorGpcsays (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" } }
}
| Field | Type and limits | Notes |
|---|---|---|
event_id | string, 1 to 128 characters | Unique 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_name | string, 3 to 128 characters | namespace.name in lower case: letters, then a dot, then letters, digits or underscores. |
schema_version | integer, 1 to 1000 | Defaults to 1. |
occurred_at | ISO 8601 timestamp | Defaults 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_id | string up to 128 characters, or null | Browser identifiers; the SDK fills them. |
subject_ref, commerce_subject_ref | string up to 256 characters, or null | Pseudonymous person references: your login id, your commerce customer id. Kept as sent on server events; on browser events set only through identity_token. |
identity_token | string up to 1,024 characters | Browser 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_ref | string up to 256 characters, or null | Join keys for orders, Kentro GO campaigns, creatives, landing pages and experiments. |
context | object | Page and device context, see below. |
consent | object | Consent snapshot. A missing purpose is unknown. |
properties | object | Event-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.
| Event | Category | Required properties | Other allowed properties | Consent purpose | Authoritative from |
|---|---|---|---|---|---|
marketing.page_viewed | marketing | none | page_type, page_name, page_path, title | analytics | Browser or server |
marketing.session_started | marketing | none | entry_url, referrer_domain, landing | analytics | Browser or server |
marketing.product_viewed | marketing | product_id | product_name, category, price (money) | analytics | Browser or server |
marketing.search_performed | marketing | query | results_count, category | analytics | Browser or server |
marketing.content_viewed | marketing | none | content_id, content_type, content_name, legacy_event | analytics | Browser or server |
marketing.cta_clicked | marketing | cta_id | cta_text, destination_url, location | analytics | Browser or server |
marketing.form_started | marketing | form_id | form_name | analytics | Browser or server |
marketing.form_submitted | marketing | form_id | form_name, fields (field names only), success, customer | analytics | Browser or server |
marketing.lead_captured | marketing | none | lead_ref, form_id, source, customer | marketing | Browser or server |
marketing.signup_completed | marketing | none | method, plan, customer | analytics | Browser or server |
marketing.email_opt_in_recorded | marketing | none | list_id, source, double_opt_in, customer | marketing | Browser or server |
marketing.experiment_exposed | marketing | none | arm | analytics | Browser or server |
marketing.asset_engaged | marketing | asset_ref | asset_type, action, seconds | analytics | Browser or server |
commerce.product_viewed | commerce | product_id | variant_id, sku, name, category, brand, price (money), in_stock | analytics | Browser or server |
commerce.collection_viewed | commerce | collection_id | name, product_count | analytics | Browser or server |
commerce.added_to_cart | commerce | product_id, quantity | variant_id, sku, name, unit_price (money), cart_id | analytics | Browser or server |
commerce.removed_from_cart | commerce | product_id, quantity | variant_id, sku, name, cart_id | analytics | Browser or server |
commerce.checkout_started | commerce | none | cart_id, checkout_id, total (money), item_count | analytics | Browser or server |
commerce.checkout_step_completed | commerce | step | checkout_id, step_name | analytics | Browser or server |
commerce.payment_attempted | commerce | none | checkout_id, payment_method, amount (money), success | analytics | Browser or server |
commerce.order_placed | commerce | order_id, total (money) | subtotal, tax, shipping, discount (money), items, item_count, coupon_code, payment_method, customer | functional | Server only |
commerce.order_paid | commerce | order_id, total (money) | payment_method, transaction_ref | functional | Server only |
commerce.order_cancelled | commerce | order_id | reason | functional | Server only |
commerce.order_refunded | commerce | order_id, amount (money) | reason, refund_ref | functional | Server only |
commerce.order_partially_refunded | commerce | order_id, amount (money) | items, reason, refund_ref | functional | Server only |
commerce.return_created | commerce | order_id | return_ref, items, reason | functional | Server only |
commerce.subscription_started | commerce | subscription_id | plan, amount (money), interval | functional | Server only |
commerce.subscription_renewed | commerce | subscription_id | amount (money), period_end | functional | Server only |
commerce.subscription_cancelled | commerce | subscription_id | reason | functional | Server only |
commerce.customer_created | commerce | customer | none | functional | Server only |
commerce.customer_updated | commerce | customer | none | functional | Server only |
commerce.fulfillment_shipped | commerce | order_id | fulfillment_ref, carrier, tracking_ref | functional | Server only |
commerce.fulfillment_delivered | commerce | order_id | fulfillment_ref, delivered_at | functional | Server only |
commerce.shipping_cost_finalized | commerce | order_id, shipping_cost (money) | carrier | functional | Server only |
marketing.campaign_impression | marketing | provider, impressions | ad_ref | marketing | Server only |
marketing.ad_clicked | marketing | provider | ad_ref, click_id_type | marketing | Browser or server |
marketing.lead_qualified | marketing | lead_ref | score, stage | marketing | Server only |
marketing.demo_booked | marketing | none | lead_ref, scheduled_at, source | marketing | Browser or server |
marketing.purchase_attributed | marketing | order_ref, model | credit (0 to 1), campaign_ref | marketing | Server only |
marketing.audience_joined | marketing | audience_ref | none | marketing | Server only |
marketing.audience_exited | marketing | audience_ref | none | marketing | Server only |
marketing.conversion_confirmed | marketing | conversion_ref | conversion_type, amount (money) | marketing | Server only |
marketing.campaign_activated | ops | none | provider, external_ref | functional | Server only |
marketing.campaign_paused | ops | none | provider, external_ref, reason | functional | Server 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,descriptionandbuiltin: 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:keyIdis the key'sid(the UUID), not thesk_key id. Revocation reaches every collector instance within about a minute: requests signed with the key then answer401 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_UNAVAILABLEanswer means Kentro cannot store secrets at the moment; try again later.
Request
POST https://pixel.kentro.live/v1/server/events
| Header | Value |
|---|---|
Content-Type | application/json. Required: the signature is checked over the raw JSON bytes, which are only captured for this content type. |
X-Kentro-Key-Id | The sk_ key id. |
X-Kentro-Timestamp | Unix time in seconds (digits only). |
X-Kentro-Signature | sha256= followed by the lower-case hex HMAC-SHA256 of timestamp + "." + rawBody, keyed with the secret. |
X-Kentro-Idempotency-Key | Optional, 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_STALEotherwise). - The comparison is constant-time; a wrong or missing signature answers
401 SIGNATURE_INVALID.
Idempotency
Two layers protect retries:
- Per event.
event_idis deduplicated per property: re-sending an event that was already accepted answersduplicatefor that event and stores nothing new. Only accepted events use up theirevent_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. Keepevent_idstable across retries and unique per real-world event (for exampleorder-1001-paid); without one, Kentro derives it from a hash of the envelope. - Per batch.
X-Kentro-Idempotency-Keybinds the key to the SHA-256 of the exact request body. The same key with the same body answers the original receipt with"replayed": truewithout evaluating the events again; a replayed receipt carries the first 100 results of the original batch. The same key with a different body answers409 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 whensettings.retention.journalMonthsasks 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:
| Status | Meaning |
|---|---|
accepted | Valid and stored for processing. |
duplicate | An event with this event_id was already received for the property; nothing new was stored. |
rejected | Invalid; not stored. reason_code says why, and the rejection appears in ingestion health. |
quarantined | Sensitive data detected; stored without its properties, never processed. |
suppressed | Consent missing or denied for the event's purpose; recorded minimized, never processed. |
Per-event reason codes:
| Code | Status | Meaning |
|---|---|---|
INVALID_ENVELOPE | rejected | Not an object, fails the envelope rules (types, lengths, the event_name pattern) or occurred_at is not a date. |
PAYLOAD_TOO_LARGE | rejected | The event is larger than 32 KB serialized. |
CLOCK_SKEW | rejected | occurred_at is more than 7 days in the past or 5 minutes in the future. |
UNKNOWN_EVENT | rejected | No builtin or custom schema for this event_name and schema_version. |
SCHEMA_VIOLATION | rejected | properties does not match the schema; reason points at the field. |
SENSITIVE_PAYLOAD | quarantined | Health terms, ICD-10 codes, secrets or card numbers were detected. |
CONSENT_DENIED | suppressed | The effective consent for the event's purpose is denied. |
GPC_OPT_OUT | suppressed | Global Privacy Control alone denied a marketing event. |
SENSITIVE_SCAN_INCOMPLETE | quarantined | The payload was too large or too deeply nested to scan completely. |
SENSITIVE_REDACTED | accepted | Authoritative 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_UNVERIFIED | accepted | Browser 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.
| HTTP | Code | When | Retry |
|---|---|---|---|
| 400 | INVALID_ENVELOPE | The body is not JSON, not an envelope or { "events": [...] }, or has no events. | No |
| 400 | BATCH_TOO_LARGE | More than 100 (browser) or 500 (server) events; the body carries max. | Split the batch |
| 401 | KEY_INVALID | Missing or unknown key, disabled property, or a key whose environment does not match the property. | No |
| 401 | KEY_REVOKED | The key was revoked. | No |
| 401 | SIGNATURE_INVALID | Missing or wrong signature, or the body was not sent as application/json. | After fixing the signing |
| 401 | TIMESTAMP_STALE | X-Kentro-Timestamp missing, not digits, or more than 300 seconds off. | After re-signing with the current time |
| 403 | ORIGIN_NOT_ALLOWED | The browser origin is not in allowed_origins. | No |
| 409 | IDEMPOTENCY_KEY_REUSED | Server route: the idempotency key was already used with a different body. | With a new key |
| 413 | PAYLOAD_TOO_LARGE | The body exceeds 64 KB (browser) or 512 KB (server). | Split the batch |
| 415 | UNSUPPORTED_MEDIA_TYPE | A content type other than application/json (or text/plain on browser routes). | No |
| 429 | RATE_LIMITED | Too 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 |
| 500 | INTERNAL_ERROR | Unexpected error. | With backoff |
| 503 | COLLECTOR_DISABLED | The collector is switched off. | Later |
| 503 | INGEST_UNAVAILABLE | Storage is temporarily unavailable. | With backoff |
| 503 | SIGNING_UNAVAILABLE | Server 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)
| Claim | Value |
|---|---|
sub | The pseudonymous reference: 1 to 256 characters, no whitespace, no @ (never an email address). |
typ | subject_ref or commerce_subject_ref: the reference the collector sets. |
exp | Expiry in Unix seconds, at most 24 hours after minting. |
kid | The 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_reforcommerce_subject_reftosubon 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:
| Setting | Default | Effect |
|---|---|---|
requireForAnalytics | true | Analytics events need analytics consent. When false, they are accepted unless analytics is explicitly denied. |
requireForMarketing | true | The same for marketing events. |
defaultWhenUnknown | denied | What unknown counts as when consent is required (granted or denied). |
honorGpc | true | Global 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 purpose | State in the snapshot | Result |
|---|---|---|
| analytics or marketing | granted | accepted |
| analytics or marketing | denied | suppressed |
| analytics or marketing | unknown | defaultWhenUnknown when the purpose is required (suppressed by default); accepted when it is not |
| marketing | any, with GPC and honorGpc | suppressed |
| functional | denied | suppressed |
| functional | granted or unknown | accepted |
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
unknownis not a choice, so a property default never becomes a decision. - Choices are ordered by the time Kentro received them; the
recorded_atyour 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 saysgpc: false. - A
consent_withdrawalprivacy 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:
| Detector | Matches |
|---|---|
health_term | A 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_code | ICD-10 style codes with a decimal part, such as E11.9 or J45.909. |
secret | sk_live_ keys, AWS access key ids (AKIA...), PEM private keys or certificates and JWT-like tokens. |
card_number | Runs 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.
| Pattern | Matches |
|---|---|
/account | The 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-password | A 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/1001becomes/[redacted]/[redacted]/[redacted], and/patient/123/labs/todaybecomes/patient/123/[redacted]/[redacted]. - Regular expressions are not accepted. A simple legacy expression such as
^/accountis converted when it is saved; anything else is refused with400. 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" }
]
}'
| Field | Values |
|---|---|
type | export (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). |
subjectKeys | 1 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:
| Type | Result |
|---|---|
export | A 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). |
delete | The 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. |
anonymize | Identifying 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_withdrawal | Every 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.sharedDevicesfor 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
failedwithresult.partial: trueand"resumable": true; Kentro retries it automatically from where it stopped. If it is stillfailedafter the automatic retries,POST /v1/privacy/requests/:id/resumequeues it again (202,"resumed": true). That route answers409 NOT_RESUMABLEfor a request that is completed, failed for good or still processing, and409 PRIVACY_JOB_ACTIVEwhile 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
destination | config (stored and returned) | credentials (write-only, encrypted at rest) |
|---|---|---|
meta_capi | pixelId (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_offline | customerId (123-456-7890 or 10 digits), conversionAction (customers/<id>/conversionActions/<id>), optional loginCustomerId, credentialSource: "go_connection", goConnectionId | none: Kentro uses your Kentro GO Google Ads connection |
ga4_mp | measurementId (G- plus 4 to 20 upper-case letters or digits) | apiSecret (8 to 256 characters) |
webhook | url (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"
}
propertyIdis optional; when set it must belong to your workspace (400 PROPERTY_NOT_FOUND).consentPurpose(defaultmarketing) is the purpose a person must have granted before anything about them is exported to this destination.testModeis ignored on creation: every destination starts withstatus: "configured"andtestMode: 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:
| Destination | Valid targets |
|---|---|
meta_capi | A 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_mp | A 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_offline | A conversion action resource name, customers/<id>/conversionActions/<id>. |
webhook | 1 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
| Status | Meaning |
|---|---|
configured | Created, exports nothing. Test sends are allowed. |
enabled | Exports mapped events. |
disabled | Switched off by you. |
error | Set 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.
| Destination | What the test does |
|---|---|
meta_capi | Sends 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_mp | Sends 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_offline | Uploads 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). |
webhook | POSTs 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:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Kentro-Pixel-Webhook/1.0 |
X-Kentro-Event | The payload type: kentro.pixel.event, kentro.pixel.audience_membership or kentro.pixel.destination_test |
X-Kentro-Delivery | Live deliveries: the delivery id, the same on every retry (use it to drop repeats) |
X-Kentro-Test | Live deliveries of a destination in test mode: 1 |
X-Kentro-Timestamp | Unix time in seconds |
X-Kentro-Signature | sha256= 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),eventand, when the matched mapping entry hasfields,data.eventcarriesevent_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_idandsession_id),context(page_path,referrer_domain,utm,device_class), the effectiveconsentandproperties.dataholds one value perfieldsentry, read from a dotted path ofevent:{ "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, andjoinedandleftlists 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.
| Status | Meaning |
|---|---|
queued | Waiting to be sent. |
sent | The 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. |
accepted | The provider accepted the request. |
rejected | The provider refused it; see error and response. |
failed | Kentro could not complete the delivery; see error. |
suppressed | Kentro 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 a200transparent 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;eventdefaults topage_view.POST /track(JSON) andPOST /track-flexible(JSON, a JSON string or a form-encoded body) answer200 { "status": "tracked", "tracking_id": "...", "event": "..." }, or400 { "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_actionandinit.
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, becomemarketing.content_viewed. The original name is kept inproperties.legacy_event. - Scalar fields of the payload (up to 50; strings cut to 512 characters) become
properties, exceptevent,keyand personal fields, which are dropped: names, postal addresses, birth dates, government ids, credentials, card data, IP addresses and user agents, and the identifier fieldsanonymous_id,visitor_id,client_id,session_id,customer_idanduser_id. page_urlorurl(else theRefererheader) becomes the page URL,page_titleortitlethe page title,referrerorrefererthe referrer,anonymous_id,client_idorvisitor_idthe anonymous id,session_idthe session id andorder_idtheorder_ref. A legacy call never sets a person reference:customer_idis not used ascommerce_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 assuppressed; functional ones such aspurchase(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 akey.
| Legacy name | Recorded as |
|---|---|
page_view | marketing.page_viewed |
product_view | commerce.product_viewed |
add_to_cart | commerce.added_to_cart |
remove_from_cart | commerce.removed_from_cart |
begin_checkout | commerce.checkout_started |
checkout_step | commerce.checkout_step_completed |
purchase | commerce.order_placed |
category_view | commerce.collection_viewed |
search_query | marketing.search_performed |
product_click, promotion_click | marketing.cta_clicked |
email_subscription | marketing.email_opt_in_recorded |
wishlist_addition | marketing.asset_engaged |
subscription_create | commerce.subscription_started |
subscription_cancel | commerce.subscription_cancelled |
account_creation | marketing.signup_completed |
init | marketing.session_started |
view_cart, filter_usage, promotion_view, promo_code_application, checkout_abandonment, subscription_pushed, login, subscription_action and unknown names | marketing.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'ssettings.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_paidwins overcommerce.order_placedfor the sameorder_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.
| Data | Kept |
|---|---|
| Event history | Expires 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 detection | 14 days |
| Rejected-event samples (ingestion health) | settings.retention.errorsDays: 30 days by default, 1 to 365 |
| Attribution results and order reconciliation records | As 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 records | Kept 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 history | A person's current memberships are kept while they last; the record of a person leaving an audience is kept 90 days |
| Raw ad click ids | 90 days, then removed from sessions and events |
| The full browser user agent of marketing-consented events | 30 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 records | 24 months after completion, as the CCPA regulations require, then deleted |
| Privacy export downloads | 24 hours |
Limits and errors
Limits
| Limit | Value |
|---|---|
Browser request body (/v1/events, /v1/events/batch) | 64 KB |
Server request body (/v1/server/events) | 512 KB |
| One event, serialized | 32 KB |
| Events per request | 100 on browser routes and the validate endpoint, 500 on the server route |
GIF beacon e parameter | 16 KB of base64url |
| Legacy request body | 1 MB |
occurred_at skew | 7 days in the past, 5 minutes in the future |
| Server signature window | 300 seconds |
| Idempotency key | 128 characters |
| Rate limit, public key | 600 events per minute, bursts up to 1,200 |
| Rate limit, server key | 6,000 events per minute, bursts up to 12,000 |
| Rate limit, client IP on browser routes and the validate endpoint | 600 requests per minute, bursts up to 1,200 |
| Rate limit, client IP on the server route | 1,200 requests per minute, bursts up to 2,400 |
| Lookups of keys not seen in the last minute, per client IP | 60 per minute, bursts up to 120 |
| Identity tokens | 5 distinct tokens per request, 1,024 characters each, valid at most 24 hours |
Duplicate detection (event_id) | At least 14 days per property |
| Allowed origins | 50 per property |
| Sensitive path patterns | 50 per property, 200 characters and 20 segments each |
| Custom event schemas | 500 per workspace are loaded |
| Destination event mapping | 50 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 keys | 1 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
| Symptom | Likely cause | What to do |
|---|---|---|
| No request leaves the page | The 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 browser | A 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_INVALID | Wrong 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_REVOKED | The key was revoked. | Create a new key and update the snippet. |
403 ORIGIN_NOT_ALLOWED | The 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_EVENT | The 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_VIOLATION | A 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_SKEW | occurred_at is far off, or was sent in milliseconds. | Send ISO 8601 timestamps from a synchronised clock. |
Result suppressed, CONSENT_DENIED or GPC_OPT_OUT | Consent 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_PAYLOAD | A 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 duplicate | The 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_LIMITED | More 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 route | The 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_STALE | The 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_UNVERIFIED | The 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_REUSED | The 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_UNAVAILABLE | The 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 destination | Processing 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 route | The 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
| Host | Serves | Authentication |
|---|---|---|
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 endpoint | Property public key or HMAC server key |
https://pixel.kentro.dev (staging) | The same collector, SDK and legacy routes on Kentro's staging environment, for testing | Property 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 testing | Kentro 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.
| Route | Permission | Purpose |
|---|---|---|
GET /v1/properties, POST /v1/properties | read, manage | List or create properties (create returns the first public key). |
GET /v1/properties/:id, PATCH /v1/properties/:id | read, manage | One property with its domains and keys; update name, origins, settings, privacy mode or status. |
POST /v1/properties/:id/domains, DELETE /v1/properties/:id/domains/:domainId | manage | Register or remove a domain. |
POST /v1/properties/:id/verify | manage | Verify a domain (dns_txt, http_file or meta_tag). |
POST /v1/properties/:id/keys, DELETE /v1/properties/:id/keys/:keyId | manage | Create or revoke a public key. |
POST /v1/properties/:id/server-keys, DELETE /v1/properties/:id/server-keys/:keyId | manage | Create (secret shown once) or revoke a server key. |
GET /v1/properties/:id/snippet | read | The install snippet and SDK URL. |
GET /v1/event-schemas | Kentro authentication, no Pixel permission | Builtin and your custom event schemas. |
POST /v1/event-schemas/validate | Kentro authentication, no Pixel permission | Validate envelopes without storing them (the collector host takes a property key instead). |
GET /v1/events/debug?propertyId&limit | read | The newest recorded events (up to 50). |
GET /v1/ingestion/health?propertyId | read | 24-hour counters, freshness and the last rejections. |
GET /v1/analytics/overview?propertyId&from&to | read | Sessions, visitors, page views, events, outcome counts, authoritative versus observed conversions, top sources and campaigns, consent suppression rate, coverage notes. |
POST /v1/analytics/funnels | read | Ordered step counts for 2 to 8 events within a window. |
GET /v1/analytics/conversions?from&to&type&limit&cursor | read | Conversion facts and totals. |
GET /v1/analytics/attribution?model&windowDays&from&to | read | Credits per channel and campaign from the latest attribution run of that model. |
POST /v1/analytics/attribution/runs | manage | Queue an attribution run (first_touch, last_touch, linear, position_based). |
GET /v1/analytics/journeys?subjectId or sessionId | read | The accepted events of one person or session, in order. |
GET /v1/analytics/data-quality?propertyId&from&to | read | Accept 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/:id | read, manage | Rule-based audiences over accepted, consented events. |
POST /v1/audiences/:id/estimate | read | Count 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/sync | manage | Queue a membership sync to webhook destinations. |
GET /v1/destinations, POST /v1/destinations, GET /v1/destinations/:id, PATCH /v1/destinations/:id, DELETE /v1/destinations/:id | read, manage | Destinations, as described above. |
POST /v1/destinations/:id/test, POST /v1/destinations/:id/sync, GET /v1/destinations/:id/deliveries | manage, manage, read | Test send, replay, delivery history. |
POST /v1/privacy/requests, GET /v1/privacy/requests, GET /v1/privacy/requests/:id | manage | Privacy requests. |
POST /v1/privacy/requests/:id/resume | manage | Queue a resumable privacy request again. |
Routes under /internal/ exist for Kentro's own services and are not reachable from the internet.
