Contents
Collector API
Send storefront events and consent to Nucleo Catalog, and read the size widget.
The Collector is the public, browser-facing door of Nucleo Catalog. A storefront sends behavioural events in batches (page and product views, searches, cart changes, checkout and purchase), reports consent changes, checks that it is connected, and reads the size recommendation widget for a product page.
It is called from shoppers' browsers — normally by the hosted script nucleo.js and by the Shopify
checkout Web Pixel — and can be called by any server for testing.
Authentication is a pair: the installation's public key (pk_ + 32 hex characters) plus an
allowed origin (one of the domains registered on the installation). The key is public and grants no
read access to your data; the domain list is what protects it.
Privacy by design: nothing is written without granted consent, a shopper's identity is only accepted
as a reference signed by your store, and it is only linked when the shopper granted the
personalisation purpose.
Authentication
publicKeyHeaderAPI key · header · X-Nucleo-KeyThe public key as a header — checked first. For servers and tools only: from a browser a custom header triggers a CORS preflight, which the Collector does not answer.
publicKeyQueryAPI key · query · keyThe installation's public key:
pk_followed by 32 lowercase hex characters. A missing or malformed key answers404.It is read, in order, from the
X-Nucleo-Keyheader, then from thekeyfield of the JSON body (the browser transport forPOST /eventsandPOST /consent, sincesendBeaconcannot set headers), then from the?key=query parameter (the usual way for the GET operations).Get it in Settings › Catalog › Search › Installations (owner or admin role): each installation shows its public key, allowed domains and a ready-to-paste
<script>snippet. Rotating the key disables the old one immediately. The key is public by design; the allowed domains are what protect it.
- Base URL
- https://api-catalog.nucleoplatform.com/api/collect/v1Production
- Who calls it
- Storefronts
- Endpoints
- 4
- OpenAPI 3.1 specification
- collector.yaml
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.
/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.
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.
key query parameter or X-Nucleo-Key headerHeaders
OriginstringSet by the browser. 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).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>with the store's storefront secret. In a Shopify theme it is produced with Liquid'shmac_sha256filter and exposed as<meta name="nucleo-customer" content="…">, which the script picks 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?key=$NUCLEO_KEY" \
-H 'Origin: https://shop.acme.example' \
-H 'Content-Type: text/plain' \
-d '{
"key": "pk_3f9a1c7e5b2d4f60a8c1e9b7d3f5a2c4",
"v": 1,
"sentAt": "2026-10-04T09:31:02.418Z",
"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",
"at": "2026-10-04T09:30:12.004Z",
"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",
"at": "2026-10-04T09:30:40.551Z",
"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",
"at": "2026-10-04T09:30:55.120Z",
"query": "black hoodie",
"payload": {
"results": 12
}
},
{
"uid": "e-7f3a9c1b23",
"kind": "add_to_cart",
"at": "2026-10-04T09:31:01.870Z",
"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?key=${process.env.NUCLEO_KEY}`, {
method: "POST",
headers: {
Origin: "https://shop.acme.example",
"Content-Type": "text/plain",
},
body: JSON.stringify({
"key": "pk_3f9a1c7e5b2d4f60a8c1e9b7d3f5a2c4",
"v": 1,
"sentAt": "2026-10-04T09:31:02.418Z",
"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",
"at": "2026-10-04T09:30:12.004Z",
"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",
"at": "2026-10-04T09:30:40.551Z",
"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",
"at": "2026-10-04T09:30:55.120Z",
"query": "black hoodie",
"payload": {
"results": 12
}
},
{
"uid": "e-7f3a9c1b23",
"kind": "add_to_cart",
"at": "2026-10-04T09:31:01.870Z",
"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?key=' . getenv('NUCLEO_KEY'), [
'headers' => [
'Origin' => 'https://shop.acme.example',
'Content-Type' => 'text/plain',
],
'json' => [
'key' => 'pk_3f9a1c7e5b2d4f60a8c1e9b7d3f5a2c4',
'v' => 1,
'sentAt' => '2026-10-04T09:31:02.418Z',
'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',
'at' => '2026-10-04T09:30:12.004Z',
'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',
'at' => '2026-10-04T09:30:40.551Z',
'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',
'at' => '2026-10-04T09:30:55.120Z',
'query' => 'black hoodie',
'payload' => [
'results' => 12,
],
],
[
'uid' => 'e-7f3a9c1b23',
'kind' => 'add_to_cart',
'at' => '2026-10-04T09:31:01.870Z',
'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."
}Consent
Consent changes for a visitor. granted creates or refreshes the visitor; denied and revoked
erase everything stored about that visitor and leave only a hashed tombstone for 30 days; without a
visitor id they are counted anonymously so you can measure opt-outs without storing anything.
/consentReport a consent change
Reports the visitor's consent state. Same transport, key, origin rules and rate limits as
POST /events.
consent | with visitor.id | without visitor.id |
|---|---|---|
granted | creates or refreshes the visitor with its purposes and source; counted once per visitor | 422 (an id is required) |
denied / revoked | if the visitor exists: deletes its events, identities and carts (carts already confirmed by Shopify's server are kept but detached from the person) and keeps a tombstone (sha256 of the id) for 30 days; always counted | counted anonymously, nothing written |
pending | nothing written, nothing counted | nothing written, nothing counted |
A later granted with the same id starts a brand-new visitor. Purposes are read from purposes, or
from visitor.purposes when absent; the source from visitor.consentSource, or consentSource.
With a test code nothing is written or counted and the response carries test: true.
Rate limits. Shared with all Collector operations: 120 requests per minute per IP, 20,000 per minute per key.
key query parameter or X-Nucleo-Key headerHeaders
OriginstringSet by the browser. 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).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
keystringThe installation's public key.
pattern ^pk_[0-9a-f]{32}$Example:pk_3f9a1c7e5b2d4f60a8c1e9b7d3f5a2c4sentAtstring (date-time)RFC 3339 with a
Zor an explicit offset, as produced byDate.prototype.toISOString().max length 40pattern ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$Example:2026-10-04T09:30:12.004ZconsentstringrequiredCase-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 ofshopifymanualgpcvisitorobjectidis required forgranted; optional fordenied,revokedandpending.Child attributes
idstringOpaque 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:8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6fconsentSourcestringWhere the consent decision came from. Any other value is ignored (stored as null).
One ofshopifymanualgpcpurposesarray 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 10
teststringTest 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:K7Q2ZP
Responses
200The consent change was applied (or counted).application/json
HeadersAccess-Control-Allow-OriginEchoes the requestOriginwhen it was present and allowed.VaryAlwaysOrigin.
visitorstring | nullrequiredThe visitor id, or null when none was sent.
consentstringrequiredCase-insensitive; surrounding spaces are ignored.
One ofgranteddeniedpendingrevokedstoredbooleanWhether a visitor row was written. Absent when
deletedis present.countedbooleanrequiredWhether the installation's anonymous consent counters moved.
deletedobjectPresent when a known visitor was erased.
Child attributes
eventsintegeridentitiesintegercartsinteger
testtruetestKnownboolean
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 consent update is not valid.
errorsmaps each field to a single message string.application/jsonmessagestringrequirederrorsobjectrequiredField 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/consent?key=$NUCLEO_KEY" \
-H 'Origin: https://shop.acme.example' \
-H 'Content-Type: text/plain' \
-d '{
"key": "pk_3f9a1c7e5b2d4f60a8c1e9b7d3f5a2c4",
"sentAt": "2026-10-04T10:02:00Z",
"visitor": {
"id": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f",
"consentSource": "shopify"
},
"consent": "revoked",
"purposes": []
}'const res = await fetch(`https://api-catalog.nucleoplatform.com/api/collect/v1/consent?key=${process.env.NUCLEO_KEY}`, {
method: "POST",
headers: {
Origin: "https://shop.acme.example",
"Content-Type": "text/plain",
},
body: JSON.stringify({
"key": "pk_3f9a1c7e5b2d4f60a8c1e9b7d3f5a2c4",
"sentAt": "2026-10-04T10:02:00Z",
"visitor": {
"id": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f",
"consentSource": "shopify"
},
"consent": "revoked",
"purposes": []
}),
});
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/consent?key=' . getenv('NUCLEO_KEY'), [
'headers' => [
'Origin' => 'https://shop.acme.example',
'Content-Type' => 'text/plain',
],
'json' => [
'key' => 'pk_3f9a1c7e5b2d4f60a8c1e9b7d3f5a2c4',
'sentAt' => '2026-10-04T10:02:00Z',
'visitor' => [
'id' => '8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f',
'consentSource' => 'shopify',
],
'consent' => 'revoked',
'purposes' => [],
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"visitor": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f",
"consent": "granted",
"stored": true,
"counted": true
}{
"visitor": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f",
"consent": "revoked",
"deleted": {
"events": 57,
"identities": 1,
"carts": 2
},
"counted": true
}{
"visitor": null,
"consent": "denied",
"stored": false,
"counted": true
}{
"visitor": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f",
"consent": "pending",
"stored": false,
"counted": false
}{
"visitor": "8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f",
"consent": "granted",
"stored": false,
"counted": false,
"test": true,
"testKnown": false
}{
"message": "Forbidden."
}{
"message": "Not found."
}{
"message": "Store is not ready."
}{
"message": "Payload too large."
}{
"message": "The consent update is not valid.",
"errors": {
"visitor.id": "visitor.id is required to grant consent: 16-64 characters of [A-Za-z0-9_-]."
}
}{
"message": "The consent update is not valid.",
"errors": {
"consent": "consent must be one of: granted, denied, pending, revoked."
}
}{
"message": "The consent update is not valid.",
"errors": {
"visitor.id": "visitor.id must be 16-64 characters of [A-Za-z0-9_-]."
}
}{
"message": "Too Many Attempts."
}Installation
Connectivity check for the installation behind a public key.
/configCheck the installation is connected
Returns the installation behind the key: the script uses it to know it is connected, and you can open it
from the browser address bar while testing. For this GET the origin is taken from Origin, or from
Referer when Origin is absent. A paused installation answers 403.
Rate limits. Shared with all Collector operations: 120 requests per minute per IP, 20,000 per minute per key.
key query parameter or X-Nucleo-Key headerHeaders
OriginstringSet by the browser. 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).nullis accepted only when the installation has the Web Pixel enabled. An origin that does not match answers403.Example:https://shop.acme.exampleRefererstringOn GET operations only, used for the origin check when
Originis absent.Example:https://shop.acme.example/products/classic-tee-black
Responses
200The installation is active and the origin is allowed.application/json
HeadersAccess-Control-Allow-OriginEchoes the requestOriginwhen it was present and allowed.VaryAlwaysOrigin.
oktruerequiredstorestringrequiredStore slug.
Example:acmeinstallationstringrequiredInstallation name.
Example:Acme EU storefrontstatusstringrequiredAlways
active(a paused installation answers403).One ofactivelocalesarray of stringrequiredpixelEnabledbooleanrequiredWhether
Origin: null(the checkout Web Pixel) is accepted.
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
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 GET "https://api-catalog.nucleoplatform.com/api/collect/v1/config?key=$NUCLEO_KEY" \
-H 'Origin: https://shop.acme.example' \
-H 'Referer: https://shop.acme.example/products/classic-tee-black' \
-H 'Accept: application/json'const res = await fetch(`https://api-catalog.nucleoplatform.com/api/collect/v1/config?key=${process.env.NUCLEO_KEY}`, {
method: "GET",
headers: {
Origin: "https://shop.acme.example",
Referer: "https://shop.acme.example/products/classic-tee-black",
Accept: "application/json",
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-catalog.nucleoplatform.com/api/collect/v1/config?key=' . getenv('NUCLEO_KEY'), [
'headers' => [
'Origin' => 'https://shop.acme.example',
'Referer' => 'https://shop.acme.example/products/classic-tee-black',
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"ok": true,
"store": "acme",
"installation": "Acme EU storefront",
"status": "active",
"locales": [
"it",
"en"
],
"pixelEnabled": true
}{
"message": "Forbidden."
}{
"message": "Not found."
}{
"message": "Store is not ready."
}{
"message": "Too Many Attempts."
}Widgets
Read-only widgets for product pages, served with the same public key, origin check and consent rules as the events.
/size/{product}Get size advice for a product
Powers a size selector on the product page: the sizes the product is sold in, how it fits (from size swaps observed across shoppers, the same for everyone) and, when every condition below holds, the size recommended to the shopper who is looking.
The personal advice appears only if:
- a
customerreference signed by your store is supplied and verifies (a plain id is ignored); - the
visitorhas granted consent including thepersonalisationpurpose (recorded viaPOST /consentor an events batch); - the customer has enough purchase history.
Otherwise the answer stays impersonal and why says which condition was missing. A failed verification
is never an error. The response never contains the customer's size profile or purchase list, and the
call is not logged against the customer.
Only published products answer with sizes: a draft, archived or status-less product (or an unknown id)
answers like a product without sizes (why: "no-sizes"), so the widget can hide itself.
Same key and origin rules as the other operations (for this GET, Referer is accepted when Origin is
absent). From a browser, pass key, visitor and customer as query parameters: custom headers trigger
a CORS preflight that the Collector does not answer. The customer reference is short-lived (15 minutes by
default), so it is harmless in access logs.
Rate limits. Shared with all Collector operations: 120 requests per minute per IP, 20,000 per minute per key.
key query parameter or X-Nucleo-Key headerPath parameters
productintegerrequiredThe Nucleo Catalog product id (numeric), not the Shopify product id.
min 1Example:4821
Query parameters
visitorstringThe visitor id used in the events batches. Used only to read the visitor's consent.
max length 64Example:8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6fcustomerstringThe signed customer reference (see
CustomerReference). Ignored when theX-Nucleo-Customer-Refheader is present.max length 512Example:v1.12.7012345678.1791104400.9c1e4b7a2f0d3c6e8b5a1f4d7c0e3b6a9d2f5c8e1b4a7d0c3f6e9b2a5d8c1f4e
Headers
X-Nucleo-Customer-RefstringThe signed customer reference, for server-side callers. Takes precedence over
customer(max 512 characters).max length 512OriginstringSet by the browser. 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).nullis accepted only when the installation has the Web Pixel enabled. An origin that does not match answers403.Example:https://shop.acme.exampleRefererstringOn GET operations only, used for the origin check when
Originis absent.Example:https://shop.acme.example/products/classic-tee-black
Responses
200Always answered, with or without a personal recommendation.application/json
HeadersAccess-Control-Allow-OriginEchoes the requestOriginwhen it was present and allowed.VaryAlwaysOrigin.
dataobjectrequiredChild attributes
productobjectrequiredChild attributes
idintegersizesarray of stringSizes the product is sold in, in wearing order. Empty for one-size, unpublished or unknown products.
readingSizeReading | nullrequiredSizeReading
directionstringOne ofruns_largeruns_smallas_expectedbasisstringproductthis item's own swaps; otherwise items of the same segment, fit (and type).One ofproductgender-fit-typegender-fitpeopleintegerShoppers whose swaps back the reading (at least 8 for a product, 40 for a family).
sharenumberShare of swaps towards the larger size.
basenumberThe same share for the whole segment, for comparison.
fitstring | nullThe product's fit attribute value.
segmentstringThe product's gender segment (
unknownwhen not set).Example:mansaysstringA ready-made English sentence.
adviceSizeAdvice | nullrequiredSizeAdvice
sizestringExample:MusualstringThe shopper's usual size on this segment and scale.
Example:LstepintegerSteps from the usual size (never more than one).
One of-101segmentstringExample:manscalestringSize scale identifier.
Example:alphabasisstringOne ofproductgender-fit-typegender-fitnonepeopleintegersharenumber | nullbasenumber | nullfitstring | nullconfidentbooleanWhether the usual size is settled (an unsettled size is never adjusted).
keptintegerItems kept in the usual size.
seenintegerItems considered.
reasonstringOne ofalignedstepunsettledgapno-largerno-smallerno-fit-datasaysstringA ready-made English sentence (third person, for staff or UI copy).
personalisedbooleanrequiredwhystring | nullrequiredWhy the answer is not personal:
anonymousno customer reference ·unverifiedthe reference did not verify ·no-consentthe visitor has not grantedpersonalisation·no-historynot enough purchase history ·no-sizesthe product has no sizes (or is not published).nullwhen personalised.One ofanonymousunverifiedno-consentno-historyno-sizesnull
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
422A query parameter is too long (standard validation error shape).application/json
messagestringrequirederrorsobjectrequired
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 GET "https://api-catalog.nucleoplatform.com/api/collect/v1/size/4821?key=$NUCLEO_KEY&visitor=8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f&customer=v1.12.7012345678.1791104400.9c1e4b7a2f0d3c6e8b5a1f4d7c0e3b6a9d2f5c8e1b4a7d0c3f6e9b2a5d8c1f4e" \
-H 'Origin: https://shop.acme.example' \
-H 'Referer: https://shop.acme.example/products/classic-tee-black' \
-H 'Accept: application/json'const res = await fetch(`https://api-catalog.nucleoplatform.com/api/collect/v1/size/4821?key=${process.env.NUCLEO_KEY}&visitor=8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f&customer=v1.12.7012345678.1791104400.9c1e4b7a2f0d3c6e8b5a1f4d7c0e3b6a9d2f5c8e1b4a7d0c3f6e9b2a5d8c1f4e`, {
method: "GET",
headers: {
Origin: "https://shop.acme.example",
Referer: "https://shop.acme.example/products/classic-tee-black",
Accept: "application/json",
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-catalog.nucleoplatform.com/api/collect/v1/size/4821?key=' . getenv('NUCLEO_KEY') . '&visitor=8b1f0c2e9d7a4b3c6e5f1a2b3c4d5e6f&customer=v1.12.7012345678.1791104400.9c1e4b7a2f0d3c6e8b5a1f4d7c0e3b6a9d2f5c8e1b4a7d0c3f6e9b2a5d8c1f4e', [
'headers' => [
'Origin' => 'https://shop.acme.example',
'Referer' => 'https://shop.acme.example/products/classic-tee-black',
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"product": {
"id": 4821,
"sizes": [
"XS",
"S",
"M",
"L",
"XL"
]
},
"reading": {
"direction": "runs_large",
"basis": "product",
"people": 64,
"share": 0.297,
"base": 0.512,
"fit": "oversize",
"segment": "man",
"says": "Runs large: 70% of the 64 shoppers who swapped size on this item kept the smaller one. Most people do better one size down from their usual."
},
"advice": {
"size": "M",
"usual": "L",
"step": -1,
"segment": "man",
"scale": "alpha",
"basis": "product",
"people": 64,
"share": 0.297,
"base": 0.512,
"fit": "oversize",
"confident": true,
"kept": 6,
"seen": 7,
"reason": "step",
"says": "Take M instead of their usual L: 70% of the 64 shoppers who swapped size on this item kept the smaller one."
},
"personalised": true,
"why": null
}
}{
"data": {
"product": {
"id": 4821,
"sizes": [
"XS",
"S",
"M",
"L",
"XL"
]
},
"reading": {
"direction": "as_expected",
"basis": "gender-fit",
"people": 412,
"share": 0.508,
"base": 0.512,
"fit": "regular",
"segment": "woman",
"says": "Fits as expected: the 412 shoppers who swapped size on 37 items that fit the same way split the same way as the rest of the range. Take your usual size."
},
"advice": null,
"personalised": false,
"why": "anonymous"
}
}{
"data": {
"product": {
"id": 4821,
"sizes": []
},
"reading": null,
"advice": null,
"personalised": false,
"why": "no-sizes"
}
}{
"message": "Forbidden."
}{
"message": "Not found."
}{
"message": "Store is not ready."
}{
"message": "The visitor field must not be greater than 64 characters.",
"errors": {
"visitor": [
"The visitor field must not be greater than 64 characters."
]
}
}{
"message": "Too Many Attempts."
}