Contents
Events
Batches of storefront events. Each batch is validated event by event: an event with an unknown kind,
a malformed uid or a timestamp out of range is rejected on its own and the rest of the batch is stored.
Only batch-level problems (no visitor id, invalid consent state, more than 50 events, bad test code,
unsupported version) fail the whole batch with a 422.
Base URL, authentication and conventions of this API are on the Collector API page.
/eventsSend a batch of events
Sends up to 50 events for one visitor in a single batch (body ≤ 64 KB).
Transport. Browsers should send the body as text/plain (e.g. navigator.sendBeacon or
fetch(..., {mode: "no-cors", credentials: "omit"})) with the key in the key body field: a
text/plain POST without custom headers is a CORS simple request and needs no preflight. The
Collector does not answer CORS preflights, so application/json or the X-Nucleo-Key header only
work from servers and tools such as curl.
Origin. On POST only the Origin header counts (a Referer alone is refused). Origin: null
(the sandboxed Shopify Web Pixel) is accepted only when the installation has the Web Pixel enabled,
and then only checkout_start and purchase events pass (other kinds are rejected with reason
origin) and the channel is recorded as web-pixel.
Consent. If visitor.consent is not granted (compared after trimming and lower-casing) nothing is
written — not even the visitor — and the response is 202 with stored: 0 and reason: "consent". If it
is denied or revoked and the visitor is already known, the erasure described in
POST /consent is applied.
Validation per event.
kindmust be one of the seven supported kinds (kind).uidis optional; when present it must match^[A-Za-z0-9_-]{8,40}$(uid).atis optional (defaults to now) and must be RFC 3339; it must fall between 7 days ago and 5 minutes from now (time). If the batch carriessentAtand the browser clock differs from ours by more than 30 seconds and at most 24 hours, everyatis shifted by the difference before the window check (the original value is kept aspayload._client_at, the shift is returned inskew).- An event that is not a JSON object is rejected (
shape). payloadmust be a flat object of scalars with at most 20 keys and strings ≤ 500 characters; otherwise the payload is dropped (the event is kept) and its index is listed inpayloadDropped. Only the keys allowed for the kind are kept (see each event schema); others are silently dropped and the event index is also listed inpayloadDropped. Keys starting with_are reserved.page_view.pathandproduct_view.fromare stored without fragment and query string (exceptq); under/account,/checkout,/checkouts,/ordersand/customeronly the first segment is kept.query(search only) is truncated to 200 characters with whitespace collapsed. A query that looks like personal data — an email address, 8+ consecutive digits, or a phone-like digit sequence (9+ digits with separators, or 6+ digits starting with+) — is withheld: stored asnull, flagged, and its index listed inwithheld.- Control characters (C0 and DEL) are stripped from every string, except tab, line feed and carriage
return: these are not stripped. In
querythey collapse to a single space like any whitespace; other strings keep them (most fields only trim them at the start and end,payloadstrings keep them as sent).
Idempotency. An event with a uid is written at most once: re-sending it (or repeating it in the
same batch) counts it in duplicates. Always set a random uid so retries are safe.
Identity. customer is a reference signed by your store (see CustomerReference). It is verified,
and the visitor is linked to the customer only if visitor.purposes contains personalisation;
otherwise the events stay anonymous and identityWithheld is true. A plain customer id is never
accepted.
Product references. ref is matched to your Catalog by variant id, then SKU, then product id, then
handle. Events whose reference does not match any product are stored anyway and counted in unmapped.
Carts. When payload.cart is present, add_to_cart / remove_from_cart / checkout_start /
purchase update the visitor's cart. A cart token that belongs to another visitor is not touched (the
event is still stored) and counted in cartRefused.
Test mode. With a test code (6 characters [A-Z0-9], generated in Catalog › Shoppers › Test
events) the batch runs the full validation and product matching but writes nothing; the result
appears live in Test events and the response carries test: true. An expired or unknown code still
keeps the batch in test mode (testKnown: false).
A database error never produces a 500: the batch comes back 202 with every event rejected with
reason storage.
Rate limits. 120 requests per minute per client IP, plus an emergency cap of 20,000 requests per minute per public key. The limits are shared by all Collector operations and are checked before the key is looked up.
X-Nucleo-Key header or key query parameterHeaders
OriginstringrequiredRequired. Browsers set it; servers and tools must send it (on GET,
Refereris accepted instead). Its host must equal one of the installation's domains, or be a subdomain of a wildcard domain (*.acme.examplematchesshop.acme.exampleandeu.shop.acme.example, notacme.example). A wildcard must sit under your own registrable domain: it is refused on a public suffix (*.com,*.co.uk) and on shared platforms where every subdomain belongs to someone else (for example*.myshopify.com,*.vercel.app,*.netlify.app,*.github.io); such a wildcard saved earlier matches nothing. Register those hosts by their exact name.nullis accepted only when the installation has the Web Pixel enabled. An origin that does not match answers403.Example:https://shop.acme.example
Request bodytext/plain, application/json
A batch of events. Send it as text/plain from browsers (the body is still JSON) or as
application/json from servers. Maximum 64 KB.
keystringThe installation's public key.
pattern ^pk_[0-9a-f]{32}$Example:pk_3f9a1c7e5b2d4f60a8c1e9b7d3f5a2c4v1 | "1"Contract version. Optional; when present must be
1(number or string).Example:1sentAtstring (date-time)Browser time when the batch was sent. Used to correct clock skew between 30 seconds and 24 hours. An unreadable value is ignored.
visitorVisitorrequiredChild attributes
idstringrequiredOpaque browser identifier generated by the storefront: Shopify's
_shopify_ycookie value (so the theme and the checkout Web Pixel share the same visitor) or 32 random hex characters. Ids starting withdemo(any case) are reserved and rejected.pattern ^[A-Za-z0-9_-]{16,64}$Example:8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6fconsentstringrequiredCase-insensitive; surrounding spaces are ignored.
One ofgranteddeniedpendingrevokedpurposesarray of stringConsent purposes. Only
personalisationchanges behaviour: without it events stay anonymous and the size widget stays impersonal. Items beyond the 10th are ignored; each item is trimmed to 32 characters. The storefront script maps Shopify'sanalyticsProcessingAllowed()toanalyticsandmarketingAllowed()topersonalisation.max items 10consentSourcestringWhere the consent decision came from. Any other value is ignored (stored as null).
One ofshopifymanualgpclocalestringDefault locale for the batch's events (truncated to 16 characters).
max length 16Example:itchannelstringDefault channel for the batch's events (truncated to 64 characters).
max length 64Example:web
sessionstringOptional session id (same format as the visitor id). A malformed value is ignored.
pattern ^[A-Za-z0-9_-]{16,64}$Example:s_4c2a9e1b7f3d5a60customerstringA customer reference signed by your store, in the form
v1.<storeId>.<shopifyCustomerId>.<expiresAt>.<signature>whereexpiresAtis a Unix timestamp (at most 24 hours ahead; 15 minutes is recommended) andsignatureis the lowercase hexHMAC-SHA256ofv1.<storeId>.<shopifyCustomerId>.<expiresAt>keyed with your store's signing secret.- Signing secret: in Nucleo, Settings › Catalog › Search › Installations, panel Customer reference secret, Generate secret (owner or admin role). It is shown once; Regenerate invalidates every reference signed with the previous one.
- storeId: the numeric id of your Nucleo store. Nucleo does not show it: ask Nucleo support.
Sign on your server, never in the theme or in browser code: whoever holds the secret can sign a reference for any customer. Your storefront's backend computes the reference for the signed-in customer and prints it into the page as
<meta name="nucleo-customer" content="…">(or the script'sdata-customerattribute), or passes it tonucleo.identify(reference); the script picks it up. Wrong store, expired, malformed or badly signed references are treated as anonymous, never as errors.max length 200Example:v1.12.7012345678.1791104400.9c1e4b7a2f0d3c6e8b5a1f4d7c0e3b6a9d2f5c8e1b4a7d0c3f6e9b2a5d8c1f4eteststringTest code from Catalog › Shoppers › Test events (valid for 60 minutes, one per store). Trimmed and upper-cased before validation. A present but malformed value fails the request with
422. The storefront script also picks it up from?nucleo_test=<code>in any page URL.pattern ^[A-Z0-9]{6}$Example:K7Q2ZPeventsarray of EventBase & object | EventBase & object | EventBase & object | EventBase & object | EventBase & object | EventBase & object | EventBase & objectThe events, in any order. An empty list is valid.
max items 50default []
Responses
202The batch was received. Inspect the counters to see what was stored: the storefront usually does not read the response (
no-cors,sendBeacon), it is there for testing. application/jsonHeadersAccess-Control-Allow-OriginEchoes the requestOriginwhen it was present and allowed.VaryAlwaysOrigin.X-RateLimit-LimitRequests allowed per minute for the client IP.X-RateLimit-RemainingRequests left in the current minute.
One of the following shapes:
EventsResult
acceptedintegerrequiredEvents that passed validation.
storedintegerrequiredEvents actually written (always
0in test mode).duplicatesintegerrequiredEvents skipped because their
uidwas already stored or repeated in the batch.rejectedarray of RejectedEventrequiredAttributes of each item
indexintegerrequiredZero-based position of the event in the batch.
Example:4reasonstringrequiredshapenot an object ·kindunsupported kind ·originkind not allowed withOrigin: null·uidmalformed uid ·timeunreadable or out-of-window timestamp ·storagethe database refused the batch.One ofshapekindoriginuidtimestorage
payloadDroppedarray of integerrequiredIndexes of events whose payload (or some of its keys) was dropped.
withheldarray of integerrequiredIndexes of search events whose query was withheld as possible personal data.
skewintegerrequiredSeconds added to every
atto correct the browser clock (0when not corrected).unmappedintegerrequiredStored events whose
refmatched no product in the Catalog.cartRefusedintegerrequiredEvents whose cart token belongs to another visitor (event stored, cart untouched). Always
0in test mode.identifiedbooleanrequiredThe customer reference verified and the visitor was linked to the customer.
identityWithheldbooleanrequiredA customer reference was sent but
personalisationwas not granted, so events stay anonymous.visitorstringrequiredThe visitor id of the batch.
testtruePresent only in test mode.
testKnownbooleanPresent only in test mode — whether the code is the store's active test code.
EventsNoConsentResult
acceptedintegerrequiredstored0requiredreason"consent"requiredrejectedarray of RejectedEventrequiredAttributes of each item
indexintegerrequiredZero-based position of the event in the batch.
Example:4reasonstringrequiredshapenot an object ·kindunsupported kind ·originkind not allowed withOrigin: null·uidmalformed uid ·timeunreadable or out-of-window timestamp ·storagethe database refused the batch.One ofshapekindoriginuidtimestorage
visitorstringrequiredtesttruetestKnownboolean
EventsStorageResult
acceptedintegerrequiredstored0requiredrejectedarray of RejectedEventrequiredAttributes of each item
indexintegerrequiredZero-based position of the event in the batch.
Example:4reasonstringrequiredshapenot an object ·kindunsupported kind ·originkind not allowed withOrigin: null·uidmalformed uid ·timeunreadable or out-of-window timestamp ·storagethe database refused the batch.One ofshapekindoriginuidtimestorage
visitorstringrequiredtesttruetestKnownboolean
403The installation is paused, or the origin is not one of its domains. Both answer the same body; the exact reason is visible to the merchant in Catalog › Shoppers › Test events. application/json
messagestringrequired
404The key is missing, malformed or unknown (no detail is given on purpose).application/json
messagestringrequired
409The store behind the key is not provisioned yet.application/json
messagestringrequired
413The body exceeds 64 KB.application/json
messagestringrequired
422The batch as a whole is not valid. Nothing is written. Note that
errorsmaps each field to a single message string (not an array). application/jsonHeadersAccess-Control-Allow-OriginEchoes the requestOriginwhen it was present and allowed.
messagestringrequirederrorsobjectrequiredField path → one message string.
429Rate limit exceeded (120 requests per minute per IP, or 20,000 per minute per key). Nothing is written. application/json
HeadersRetry-AfterSeconds to wait before retrying.X-RateLimit-LimitRequests allowed per minute for the client IP.X-RateLimit-RemainingAlways0on a 429.X-RateLimit-ResetUnix timestamp at which the window resets.
messagestringrequired
curl -X POST https://api-catalog.nucleoplatform.com/api/collect/v1/events \
-H "X-Nucleo-Key: $NUCLEO_COLLECTOR_KEY" \
-H 'Origin: https://shop.acme.example' \
-H 'Content-Type: text/plain' \
-d '{
"v": 1,
"visitor": {
"id": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f",
"consent": "granted",
"purposes": [
"analytics",
"personalisation"
],
"consentSource": "shopify",
"locale": "it",
"channel": "web"
},
"session": "s_4c2a9e1b7f3d5a60",
"customer": "v1.12.7012345678.1791104400.9c1e4b7a2f0d3c6e8b5a1f4d7c0e3b6a9d2f5c8e1b4a7d0c3f6e9b2a5d8c1f4e",
"events": [
{
"uid": "e-7f3a9c1b20",
"kind": "page_view",
"market": "IT",
"payload": {
"type": "collection",
"path": "/collections/men",
"collection": "men",
"filters": "filter.v.option.color=Black;filter.v.price.lte=80",
"sort": "price-ascending"
}
},
{
"uid": "e-7f3a9c1b21",
"kind": "product_view",
"ref": {
"product": "8123456789",
"variant": "44001234567",
"handle": "classic-tee-black",
"sku": "TEE-BLK-M"
},
"payload": {
"price": "29.00",
"currency": "EUR",
"pos": 3,
"list": "collection:men",
"from": "/collections/men"
}
},
{
"uid": "e-7f3a9c1b22",
"kind": "search",
"query": "black hoodie",
"payload": {
"results": 12
}
},
{
"uid": "e-7f3a9c1b23",
"kind": "add_to_cart",
"ref": {
"variant": "44001234567",
"sku": "TEE-BLK-M"
},
"payload": {
"cart": "c1-9f2b7e4a1d",
"quantity": 1,
"price": "29.00",
"currency": "EUR"
}
}
]
}'const res = await fetch("https://api-catalog.nucleoplatform.com/api/collect/v1/events", {
method: "POST",
headers: {
"X-Nucleo-Key": `${process.env.NUCLEO_COLLECTOR_KEY}`,
Origin: "https://shop.acme.example",
"Content-Type": "text/plain",
},
body: JSON.stringify({
"v": 1,
"visitor": {
"id": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f",
"consent": "granted",
"purposes": [
"analytics",
"personalisation"
],
"consentSource": "shopify",
"locale": "it",
"channel": "web"
},
"session": "s_4c2a9e1b7f3d5a60",
"customer": "v1.12.7012345678.1791104400.9c1e4b7a2f0d3c6e8b5a1f4d7c0e3b6a9d2f5c8e1b4a7d0c3f6e9b2a5d8c1f4e",
"events": [
{
"uid": "e-7f3a9c1b20",
"kind": "page_view",
"market": "IT",
"payload": {
"type": "collection",
"path": "/collections/men",
"collection": "men",
"filters": "filter.v.option.color=Black;filter.v.price.lte=80",
"sort": "price-ascending"
}
},
{
"uid": "e-7f3a9c1b21",
"kind": "product_view",
"ref": {
"product": "8123456789",
"variant": "44001234567",
"handle": "classic-tee-black",
"sku": "TEE-BLK-M"
},
"payload": {
"price": "29.00",
"currency": "EUR",
"pos": 3,
"list": "collection:men",
"from": "/collections/men"
}
},
{
"uid": "e-7f3a9c1b22",
"kind": "search",
"query": "black hoodie",
"payload": {
"results": 12
}
},
{
"uid": "e-7f3a9c1b23",
"kind": "add_to_cart",
"ref": {
"variant": "44001234567",
"sku": "TEE-BLK-M"
},
"payload": {
"cart": "c1-9f2b7e4a1d",
"quantity": 1,
"price": "29.00",
"currency": "EUR"
}
}
]
}),
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('POST', 'https://api-catalog.nucleoplatform.com/api/collect/v1/events', [
'headers' => [
'X-Nucleo-Key' => getenv('NUCLEO_COLLECTOR_KEY'),
'Origin' => 'https://shop.acme.example',
'Content-Type' => 'text/plain',
],
'json' => [
'v' => 1,
'visitor' => [
'id' => '8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f',
'consent' => 'granted',
'purposes' => [
'analytics',
'personalisation',
],
'consentSource' => 'shopify',
'locale' => 'it',
'channel' => 'web',
],
'session' => 's_4c2a9e1b7f3d5a60',
'customer' => 'v1.12.7012345678.1791104400.9c1e4b7a2f0d3c6e8b5a1f4d7c0e3b6a9d2f5c8e1b4a7d0c3f6e9b2a5d8c1f4e',
'events' => [
[
'uid' => 'e-7f3a9c1b20',
'kind' => 'page_view',
'market' => 'IT',
'payload' => [
'type' => 'collection',
'path' => '/collections/men',
'collection' => 'men',
'filters' => 'filter.v.option.color=Black;filter.v.price.lte=80',
'sort' => 'price-ascending',
],
],
[
'uid' => 'e-7f3a9c1b21',
'kind' => 'product_view',
'ref' => [
'product' => '8123456789',
'variant' => '44001234567',
'handle' => 'classic-tee-black',
'sku' => 'TEE-BLK-M',
],
'payload' => [
'price' => '29.00',
'currency' => 'EUR',
'pos' => 3,
'list' => 'collection:men',
'from' => '/collections/men',
],
],
[
'uid' => 'e-7f3a9c1b22',
'kind' => 'search',
'query' => 'black hoodie',
'payload' => [
'results' => 12,
],
],
[
'uid' => 'e-7f3a9c1b23',
'kind' => 'add_to_cart',
'ref' => [
'variant' => '44001234567',
'sku' => 'TEE-BLK-M',
],
'payload' => [
'cart' => 'c1-9f2b7e4a1d',
'quantity' => 1,
'price' => '29.00',
'currency' => 'EUR',
],
],
],
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"accepted": 4,
"stored": 3,
"duplicates": 1,
"rejected": [
{
"index": 4,
"reason": "kind"
}
],
"payloadDropped": [
1
],
"withheld": [],
"skew": 0,
"unmapped": 0,
"cartRefused": 0,
"identified": true,
"identityWithheld": false,
"visitor": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f"
}{
"accepted": 3,
"stored": 0,
"reason": "consent",
"rejected": [],
"visitor": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f"
}{
"accepted": 1,
"stored": 0,
"duplicates": 0,
"rejected": [],
"payloadDropped": [],
"withheld": [],
"skew": 0,
"unmapped": 0,
"cartRefused": 0,
"identified": false,
"identityWithheld": false,
"visitor": "4e6c1a9b2d7f3e5a8c0b1d2e3f4a5b6c",
"test": true,
"testKnown": true
}{
"accepted": 2,
"stored": 0,
"rejected": [
{
"index": 0,
"reason": "storage"
},
{
"index": 1,
"reason": "storage"
}
],
"visitor": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f"
}{
"message": "Forbidden."
}{
"message": "Not found."
}{
"message": "Store is not ready."
}{
"message": "Payload too large."
}{
"message": "The batch is not valid.",
"errors": {
"visitor.id": "visitor.id is required: 16-64 characters of [A-Za-z0-9_-]."
}
}{
"message": "The batch is not valid.",
"errors": {
"events": "A batch holds at most 50 events."
}
}{
"message": "The batch is not valid.",
"errors": {
"visitor.consent": "visitor.consent must be one of: granted, denied, pending, revoked.",
"test": "test must be a 6-character code of [A-Z0-9]."
}
}{
"message": "The batch is not valid.",
"errors": {
"visitor.id": "visitor.id must not start with \"demo\": the prefix is reserved for the simulated signal."
}
}{
"message": "The batch is not valid.",
"errors": {
"v": "Unsupported batch version: only v=1 is accepted."
}
}{
"message": "Too Many Attempts."
}