Forward conversion events to Meta, Google Ads and GA4, and events or audience membership to your own signed webhooks.
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.
In this page
- How delivery works
- Destination types
- Create a destination
- Event mapping
- Lifecycle and test mode
- Test sends
- Google Ads offline conversions
- Webhooks
- Deliveries and what accepted means
- Replays
How delivery works
- Mapped events of an enabled destination are delivered in the background. Each delivery is retried with exponential backoff (2^attempts x 30 seconds, at most 1 hour; a provider's
Retry-Afteris honored instead) for up to 8 attempts, and every outcome is listed in the 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
consentPurposethe destination stores. - Properties in
strictprivacy mode never send to ad networks. Quarantined events and redacted server events are never sent. - The event's own consent purpose never authorizes an export: a functional server order is collected without marketing consent, but reaches Meta or Google Ads only for a person with an explicit marketing grant.
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 (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. In Kentro GO, the Pixel page's Destinations tab creates and edits all four types (click New destination).
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. GA4 and webhook destinations follow it; ad networks always require an explicit marketing grant and no GPC signal.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). source must be a builtin or an active custom event of your workspace, and neither a reserved event nor the deprecated marketing.product_viewed. A mapping saved earlier on marketing.product_viewed keeps matching commerce.product_viewed events. Duplicates of the same source and target are refused; enabled defaults to true.
| 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/:idand{ "status": "enabled" }. Enabling requires stored credentials (or a linked Kentro GO connection), otherwise409 CREDENTIALS_REQUIRED, and at least one enabled mapping, otherwise409 EVENT_MAPPING_REQUIRED. Ameta_capidestination in test mode also needsconfig.testEventCode, otherwise400 TEST_EVENT_CODE_REQUIRED. PATCHalso updatesname,propertyId,config,credentials(nullclears the stored secret),eventMapping,consentPurposeandtestMode.DELETE /v1/destinations/:idremoves the destination and keeps its delivery history.testModestartstrue;PATCHwith{ "testMode": false }turns it off. While it is on, an enabled destination delivers only as tests: Meta deliveries carryconfig.testEventCodeand appear only in Events Manager's Test Events, GA4 deliveries go to the validation endpoint and Google Ads uploads run withvalidateOnly(both record nothing), and webhook requests carry"test": trueand anX-Kentro-Test: 1header.
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) with the testEventCode from the body, so it shows only under Test Events. Without { "testEventCode": "..." } the test answers 400 TEST_EVENT_CODE_REQUIRED. |
ga4_mp | Sends one event to the Measurement Protocol validation endpoint and returns its validation messages. The delivery row is sent with validateOnly: true (or rejected). |
google_ads_offline | Uploads one click conversion with validateOnly: true using an access token from your Kentro GO Google Ads connection. |
webhook | POSTs a signed synthetic payload (kentro.pixel.destination_test) 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."
}
| Error | When |
|---|---|
409 CREDENTIALS_REQUIRED | No stored secret. |
400 TEST_EVENT_CODE_REQUIRED | Meta test without a test event code. |
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, 502 GO_API_ERROR | Google could not refresh the access token right now, or the Kentro GO call failed. |
503 GO_API_UNAVAILABLE | Google Ads sending through Kentro GO is not configured on this environment. |
422 WEBHOOK_TARGET_NOT_ALLOWED | The webhook URL fails the rules below. |
503 CREDENTIALS_UNAVAILABLE | Stored credentials cannot be opened. |
Google Ads offline conversions
- Set
credentialSource: "go_connection"and the connection's id ingoConnectionId(connections are listed byGET https://apis.kentro.live/go/v1/connections). No Google Ads developer token is needed: Kentro sends conversions with the OAuth access token of your connection and never stores Google tokens on the destination. conversionActionis the resource name of an upload-type conversion action;loginCustomerIdis the manager account id when the connection signs in through one.- Mapped conversions are uploaded as click conversions and need a Google click id (
gclid,gbraidorwbraid) 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. 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, 5xx and network errors are retried; any other answer is recorded as rejected. Kentro reads at most the first 4 KB of your response.
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Kentro-Pixel-Webhook/1.0 |
X-Kentro-Event | 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,eventand, when the matched mapping hasfields,data.eventcarriesevent_id,event_name,occurred_at,received_at,source_type,authoritative,refs,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):
{
"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; an audience sync aimed at a Meta, Google Ads or GA4 destination 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, validateOnly, attempts, requestRedacted, response, 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 and validate-only test calls. |
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: consent_not_granted, gpc_opt_out, strict_privacy_mode, quarantined, sensitive_event, no_click_id, 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.
Replays
POST /v1/destinations/:id/sync with an optional { "from", "to" } window queues a replay of the window's events for an enabled destination and answers 202 with a jobId.
- Default window: the last 7 days. At most 180 days long and starting at most 30 days back;
YYYY-MM-DDvalues are whole UTC days with an inclusive end. - A destination that is not enabled answers
409 DESTINATION_NOT_ENABLED. - Replay recognizes events it already delivered by their delivery history, which is kept for 30 days, so a
fromolder than that answers400 REPLAY_RANGE_TOO_OLD.

