Contents
Webhooks
Up to 10 HTTPS endpoints per key, each with the events it wants and its own signing secret. Webhooks can be managed with a read-only key too: receiving news does not change the catalog.
Base URL, authentication and conventions of this API are on the Catalog API page.
/webhooksList webhooks
Responses
200The webhooks of this key (without secrets).application/json
dataarray of WebhookrequiredAttributes of each item
idstring (uuid)urlstringeventsarray of stringOne ofproduct.createdproduct.updatedproduct.deletedstock.updatedactivebooleanlast_success_atstring (date-time) | nulllast_failure_atstring (date-time) | nulllast_errorstring | nullExample:HTTP 503created_atstring (date-time)
curl -X GET https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN" \
-H 'Accept: application/json'const res = await fetch("https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
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/catalog/v1/webhooks', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": [
{
"id": "9b2f4c1e-5d7a-4e8b-9c3d-2a1f0e6b7c8d",
"url": "string",
"events": [
"product.created"
],
"active": true,
"last_success_at": "2026-10-01T09:30:00Z",
"last_failure_at": "2026-10-01T09:30:00Z",
"last_error": "HTTP 503",
"created_at": "2026-10-01T09:30:00Z"
}
]
}/webhooksCreate a webhook
The URL must be https and resolve to a public address. The answer contains the signing secret
(whsec_…) — the only time it is shown.
Headers
Idempotency-KeystringAny string of 1–255 printable ASCII characters, unique per change you mean (a UUID is fine). Sending the same request again with the same key within 48 hours returns the first answer without doing the work again (header
Idempotent-Replayed: true). The same key with a different request is a409 idempotency_key_reused; while the first request is still running,409 idempotency_key_in_progresswithRetry-After: 1. Answers429and5xxare not stored: retry them with the same key.max length 255Example:2b5c0d2e-7d2a-4f6e-9a51-0c8a1e7f3b44
Request bodyapplication/json
urlstring (uri)requiredmax length 500eventsarray of stringrequiredOne ofproduct.createdproduct.updatedproduct.deletedstock.updatedmin items 1
Responses
201Created. Store the secret now.application/json
dataWebhook & objectrequiredChild attributes
idstring (uuid)urlstringeventsarray of stringOne ofproduct.createdproduct.updatedproduct.deletedstock.updatedactivebooleanlast_success_atstring (date-time) | nulllast_failure_atstring (date-time) | nulllast_errorstring | nullExample:HTTP 503created_atstring (date-time)secretstringrequiredShown only now.
Example:whsec_3KQ9…
400The request must change:
validation_failed(seedetails, one list of messages per field),invalid_json,invalid_cursor,product_not_written,variant_not_written,not_written,image_not_downloadable. application/jsonerrorobjectrequiredChild attributes
codestringrequiredStable machine-readable code. Branch on it, not on the message.
messagestringrequiredrequest_idstringrequiredSame as the
X-Request-Idheader. Quote it to support.detailsobject
409
field_rule(the key's field rule does not allow this change),field_locked(the change touches fields another connected system owns;details.lockedlists them),idempotency_key_reused,idempotency_key_in_progress,webhook_limit. application/jsonerrorobjectrequiredChild attributes
codestringrequiredStable machine-readable code. Branch on it, not on the message.
messagestringrequiredrequest_idstringrequiredSame as the
X-Request-Idheader. Quote it to support.detailsobject
curl -X POST https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN" \
-H 'Idempotency-Key: 2b5c0d2e-7d2a-4f6e-9a51-0c8a1e7f3b44' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://erp.acme.example/nucleo/webhooks",
"events": [
"product.created",
"product.updated",
"product.deleted",
"stock.updated"
]
}'const res = await fetch("https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
"Idempotency-Key": "2b5c0d2e-7d2a-4f6e-9a51-0c8a1e7f3b44",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://erp.acme.example/nucleo/webhooks",
"events": [
"product.created",
"product.updated",
"product.deleted",
"stock.updated"
]
}),
});
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/catalog/v1/webhooks', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
'Idempotency-Key' => '2b5c0d2e-7d2a-4f6e-9a51-0c8a1e7f3b44',
],
'json' => [
'url' => 'https://erp.acme.example/nucleo/webhooks',
'events' => [
'product.created',
'product.updated',
'product.deleted',
'stock.updated',
],
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"id": "9b2f4c1e-5d7a-4e8b-9c3d-2a1f0e6b7c8d",
"url": "string",
"events": [
"product.created"
],
"active": true,
"last_success_at": "2026-10-01T09:30:00Z",
"last_failure_at": "2026-10-01T09:30:00Z",
"last_error": "HTTP 503",
"created_at": "2026-10-01T09:30:00Z",
"secret": "whsec_3KQ9…"
}
}{
"error": {
"code": "validation_failed",
"message": "The request is not valid: see details.",
"request_id": "7c0f8a1e-3b2d-4e5f-9a6b-1c2d3e4f5a6b",
"details": {
"variants.0.prices.0.price_list": [
"Unknown price list \"usa\". GET /price-lists lists them."
]
}
}
}{
"error": {
"code": "string",
"message": "string",
"request_id": "string",
"details": {}
}
}/webhooks/{webhook}Get a webhook
Path parameters
webhookstring (uuid)required
Responses
200The webhook, with the outcome of its last deliveries.application/json
dataWebhookrequiredChild attributes
idstring (uuid)urlstringeventsarray of stringOne ofproduct.createdproduct.updatedproduct.deletedstock.updatedactivebooleanlast_success_atstring (date-time) | nulllast_failure_atstring (date-time) | nulllast_errorstring | nullExample:HTTP 503created_atstring (date-time)
404
product_not_found,variant_not_found,image_not_found,import_not_found,webhook_not_found, ornot_foundfor a path that does not exist.application/jsonerrorobjectrequiredChild attributes
codestringrequiredStable machine-readable code. Branch on it, not on the message.
messagestringrequiredrequest_idstringrequiredSame as the
X-Request-Idheader. Quote it to support.detailsobject
curl -X GET 'https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks/{webhook}' \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN" \
-H 'Accept: application/json'const res = await fetch("https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks/{webhook}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
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/catalog/v1/webhooks/{webhook}', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"id": "9b2f4c1e-5d7a-4e8b-9c3d-2a1f0e6b7c8d",
"url": "string",
"events": [
"product.created"
],
"active": true,
"last_success_at": "2026-10-01T09:30:00Z",
"last_failure_at": "2026-10-01T09:30:00Z",
"last_error": "HTTP 503",
"created_at": "2026-10-01T09:30:00Z"
}
}{
"error": {
"code": "string",
"message": "string",
"request_id": "string",
"details": {}
}
}/webhooks/{webhook}Update a webhook or rotate its secret
With rotate_secret: true the answer contains the new secret; the old one stops working at once.
Path parameters
webhookstring (uuid)required
Headers
Idempotency-KeystringAny string of 1–255 printable ASCII characters, unique per change you mean (a UUID is fine). Sending the same request again with the same key within 48 hours returns the first answer without doing the work again (header
Idempotent-Replayed: true). The same key with a different request is a409 idempotency_key_reused; while the first request is still running,409 idempotency_key_in_progresswithRetry-After: 1. Answers429and5xxare not stored: retry them with the same key.max length 255Example:2b5c0d2e-7d2a-4f6e-9a51-0c8a1e7f3b44
Request bodyapplication/json
urlstring (uri)max length 500eventsarray of stringOne ofproduct.createdproduct.updatedproduct.deletedstock.updatedmin items 1activebooleanAn inactive webhook receives nothing; events of that time are not sent later.
rotate_secretboolean
Responses
200Updated.application/json
dataWebhook & objectrequiredChild attributes
idstring (uuid)urlstringeventsarray of stringOne ofproduct.createdproduct.updatedproduct.deletedstock.updatedactivebooleanlast_success_atstring (date-time) | nulllast_failure_atstring (date-time) | nulllast_errorstring | nullExample:HTTP 503created_atstring (date-time)secretstringOnly after
rotate_secret.
400The request must change:
validation_failed(seedetails, one list of messages per field),invalid_json,invalid_cursor,product_not_written,variant_not_written,not_written,image_not_downloadable. application/jsonerrorobjectrequiredChild attributes
codestringrequiredStable machine-readable code. Branch on it, not on the message.
messagestringrequiredrequest_idstringrequiredSame as the
X-Request-Idheader. Quote it to support.detailsobject
404
product_not_found,variant_not_found,image_not_found,import_not_found,webhook_not_found, ornot_foundfor a path that does not exist.application/jsonerrorobjectrequiredChild attributes
codestringrequiredStable machine-readable code. Branch on it, not on the message.
messagestringrequiredrequest_idstringrequiredSame as the
X-Request-Idheader. Quote it to support.detailsobject
curl -X PATCH 'https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks/{webhook}' \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN" \
-H 'Idempotency-Key: 2b5c0d2e-7d2a-4f6e-9a51-0c8a1e7f3b44' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://shop.acme.example",
"events": [
"product.created"
],
"active": true,
"rotate_secret": true
}'const res = await fetch("https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks/{webhook}", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
"Idempotency-Key": "2b5c0d2e-7d2a-4f6e-9a51-0c8a1e7f3b44",
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://shop.acme.example",
"events": [
"product.created"
],
"active": true,
"rotate_secret": true
}),
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('PATCH', 'https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks/{webhook}', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
'Idempotency-Key' => '2b5c0d2e-7d2a-4f6e-9a51-0c8a1e7f3b44',
],
'json' => [
'url' => 'https://shop.acme.example',
'events' => [
'product.created',
],
'active' => true,
'rotate_secret' => true,
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"id": "9b2f4c1e-5d7a-4e8b-9c3d-2a1f0e6b7c8d",
"url": "string",
"events": [
"product.created"
],
"active": true,
"last_success_at": "2026-10-01T09:30:00Z",
"last_failure_at": "2026-10-01T09:30:00Z",
"last_error": "HTTP 503",
"created_at": "2026-10-01T09:30:00Z",
"secret": "string"
}
}{
"error": {
"code": "validation_failed",
"message": "The request is not valid: see details.",
"request_id": "7c0f8a1e-3b2d-4e5f-9a6b-1c2d3e4f5a6b",
"details": {
"variants.0.prices.0.price_list": [
"Unknown price list \"usa\". GET /price-lists lists them."
]
}
}
}{
"error": {
"code": "string",
"message": "string",
"request_id": "string",
"details": {}
}
}/webhooks/{webhook}Delete a webhook
Path parameters
webhookstring (uuid)required
Responses
204Deleted. Pending deliveries are dropped.
404
product_not_found,variant_not_found,image_not_found,import_not_found,webhook_not_found, ornot_foundfor a path that does not exist.application/jsonerrorobjectrequiredChild attributes
codestringrequiredStable machine-readable code. Branch on it, not on the message.
messagestringrequiredrequest_idstringrequiredSame as the
X-Request-Idheader. Quote it to support.detailsobject
curl -X DELETE 'https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks/{webhook}' \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN"const res = await fetch("https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks/{webhook}", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('DELETE', 'https://api-catalog.nucleoplatform.com/api/catalog/v1/webhooks/{webhook}', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();HTTP 204 — Deleted. Pending deliveries are dropped.{
"error": {
"code": "string",
"message": "string",
"request_id": "string",
"details": {}
}
}catalogEventSomething changed in the catalog
Sent to every active webhook that subscribed to the event. The notice is small: it says what changed
and you read the product with GET /products/{id} if you need it.
product.created— a product was created, or restored from the trash.product.updated— a product changed;changed_fieldsnames the fields (for exampletitle,description,price:eu,attr:color,images,stockfor the in/out of stock state).product.deleted— a product was moved to the trash.stock.updated— the available quantity of a variant at a location changed (on the product page, by an import or through this API).
Changes made by any key, including yours, are sent: triggered_by_this_key is true when the
change came from the key that owns the webhook, so you can skip your own echoes.
Signature. X-Nucleo-Signature: t=<unix time>,v1=<signature>, where the signature is the lowercase
hex HMAC-SHA256 of <t>.<raw body> keyed with the webhook's secret. Verify it on the exact bytes
received, compare in constant time and reject a t older than 5 minutes.
Delivery. 10-second timeout, redirects not followed; any 2xx is a success. Deliveries leave
within about a minute of the change. Any other answer is retried about 30 s, 1, 2, 4, 8, 16 and 32
minutes later (8 attempts in all), then the event is dropped. Every attempt carries the same id
(also in X-Nucleo-Webhook-Id) and its number in X-Nucleo-Attempt. Events can arrive out of order:
compare created_at, or re-read the product.
Headers
X-Nucleo-SignaturestringrequiredExample:t=1791115200,v1=5f2b6c0e9a8d4f7b1c3e2a6d9b0f8e7c6a5d4b3c2e1f0a9b8c7d6e5f4a3b2c1dX-Nucleo-EventstringrequiredOne ofproduct.createdproduct.updatedproduct.deletedstock.updatedExample:product.createdX-Nucleo-Webhook-Idstring (uuid)requiredX-Nucleo-Attemptintegerrequiredmin 1max 8
Request Nucleo sendsapplication/json
idstring (uuid)requiredSame at every attempt.
typestringrequiredOne ofproduct.createdproduct.updatedproduct.deletedstock.updatedcreated_atstring (date-time)requiredtriggered_by_this_keybooleanrequireddataobjectrequiredChild attributes
product_idintegerskustring | nullchanged_fieldsarray of stringOnly in
product.updated.variant_idintegerOnly in
stock.updated.variant_skustring | nulllocationstringavailableinteger | null
Expected response
200Received. Any
2xxworks; the body is ignored.
{
"id": "5d1c9a0e-2b7f-4c3a-9e8d-6f1a2b3c4d5e",
"type": "product.updated",
"created_at": "2026-10-14T08:00:00Z",
"triggered_by_this_key": false,
"data": {
"product_id": 1201,
"sku": "TEE-001",
"changed_fields": [
"title",
"price:eu"
]
}
}{
"id": "0a9b8c7d-6e5f-4a3b-2c1d-0e9f8a7b6c5d",
"type": "stock.updated",
"created_at": "2026-10-14T08:00:05Z",
"triggered_by_this_key": true,
"data": {
"product_id": 1201,
"sku": "TEE-001",
"variant_id": 3304,
"variant_sku": "TEE-001-M",
"location": "main",
"available": 11
}
}HTTP 200 — Received. Any `2xx` works; the body is ignored.