Contents
Shipment Tracking API
Public parcel tracking for shoppers, and the inbound webhook carriers use to report tracking events.
Two sides of shipment tracking in Nucleo Commerce:
- Public tracking — the API behind the Nucleo-hosted tracking page
(
https://app.nucleoplatform.com/track/{token}): brand, order, status, estimated delivery, items and the event history of one shipment, addressed by a signed token. No login; no personal data beyond the shopper's first name and destination city. Use it to show tracking inside your own storefront. - Tracking webhook — the endpoint a tracking aggregator (Qapla'), DHL, or any other carrier (or your own middleware) calls to push parcel events into Nucleo. Each call is authenticated with the secret of the carrier connection it is addressed to.
Tracking events are normalised to one set of statuses for every carrier, update the shipment, mark orders as delivered (which starts the return window), open anomalies for problems and trigger the merchant's customer notifications (out for delivery, delivered, ready for pickup, delivery problem).
Authentication
nucleoSignatureAPI key · header · X-Nucleo-SignatureGeneric carrier connections. Lowercase hex HMAC-SHA256 of the exact raw request body, keyed with the connection's webhook secret. Compute it over the bytes you send, after JSON serialisation.
dhlWebhookKeyAPI key · query · keyDHL connections. The connection's webhook secret, as a query parameter.
- Base URL
- https://api-commerce.nucleoplatform.com/api/oms/v1Production
- Who calls it
- Storefronts, Carriers
- Endpoints
- 2
- OpenAPI 3.1 specification
- tracking.yaml
Public tracking
Read-only tracking of one shipment by its signed token.
Where the token comes from. Nucleo builds the tracking link …/track/{token} for every shipped
parcel; it is the link in the shipping notifications Nucleo sends to shoppers and the tracking page
link on the shipment in Orders in Nucleo. The token is the last path segment. It is signed by
Nucleo, cannot be guessed or altered, and does not expire; it stops working only if the shipment is
voided.
/public/tracking/{token}Get a shipment's public tracking
Status, dates, carrier and up to 50 most recent tracking events (newest first) of one shipment, with the merchant's branding. Only what the shopper needs: no email, phone or full address.
status is the normalised status of the latest event; before any event it is picked_up
(or delivered if the shipment is already marked delivered).
Language: lang if supported, otherwise the order's language, otherwise one derived from the
shipping country, otherwise en. It is returned as locale; event descriptions are shown as the
carrier sent them.
Rate limit: 60 requests per minute per IP (X-RateLimit-* headers on every
response). Read-only and idempotent.
Path parameters
tokenstringrequiredSigned tracking token, the last segment of the tracking link.
min length 20max length 200pattern ^[A-Za-z0-9_.-]{20,200}$Example:MTA0Mjo3ZjNjMmE5MS00YjZkLTRlMmEtOWMxZi0zZDhlNWEyYjZjNDA.Qm9Yc1Z3Tm1LcFJ0WmFHaEpk
Query parameters
langstringPreferred language.
One ofitendefresExample:it
Responses
200Tracking information.application/json
HeadersX-RateLimit-LimitRequests allowed per minute.X-RateLimit-RemainingRequests left in the current minute.
dataPublicTrackingrequiredChild attributes
brandobjectrequiredChild attributes
namestringrequiredExample:Acme Apparellogo_urlstring (uri) | nullrequiredaccent_colorstringrequiredHex colour (
#111111when not set).Example:#0F766Esupport_emailstring (email) | nullrequiredsupport_urlstring (uri) | nullrequired
localestringrequiredOne ofitendefresorder_namestringrequiredExample:#1042customer_first_namestringrequiredShipping first name, or empty.
Example:GiuliadestinationstringrequiredCity, COUNTRYof the shipping address (either part may be missing).Example:Milano, ITcarrierstring | nullrequiredExample:DHL Expresstracking_numbersarray of stringrequiredOne per parcel.
carrier_urlstring (uri) | nullrequiredThe carrier's own tracking page, if known.
statusstringrequiredNormalised parcel status, the same for every carrier:
pending(label created, not yet collected),picked_up,in_transit,out_for_delivery,delivered,exception(delivery problem),held(at a depot or pickup point),returned_to_sender.One ofpendingpicked_upin_transitout_for_deliverydeliveredexceptionheldreturned_to_senderstatus_atstring (date-time) | nullrequiredshipped_atstring (date-time) | nullrequireddelivered_atstring (date-time) | nullrequiredestimated_delivery_atstring (date-time) | nullrequireditemsarray of objectrequiredItems in this shipment.
Attributes of each item
titlestringrequiredquantityintegerrequiredmin 1
eventsarray of objectrequiredMost recent first.
max items 50Attributes of each item
occurred_atstring (date-time) | nullrequiredstatusstringrequiredNormalised parcel status, the same for every carrier:
pending(label created, not yet collected),picked_up,in_transit,out_for_delivery,delivered,exception(delivery problem),held(at a depot or pickup point),returned_to_sender.One ofpendingpicked_upin_transitout_for_deliverydeliveredexceptionheldreturned_to_senderdescriptionstring | nullrequiredlocationstring | nullrequired
404
not_found: invalid or tampered token, unknown store, unknown or voided shipment. A token that does not match the path pattern gets the generic route-not-found message instead. application/jsonOne of the following shapes:
TrackingError
error"not_found"required
Message
messagestringrequired
429Too many requests from this IP.application/json
HeadersRetry-AfterSeconds to wait.X-RateLimit-LimitRequests allowed per minute.X-RateLimit-RemainingRequests left in the current minute.X-RateLimit-ResetUnix time when the limit resets.
messagestringrequired
curl -X GET 'https://api-commerce.nucleoplatform.com/api/oms/v1/public/tracking/MTA0Mjo3ZjNjMmE5MS00YjZkLTRlMmEtOWMxZi0zZDhlNWEyYjZjNDA.Qm9Yc1Z3Tm1LcFJ0WmFHaEpk?lang=it' \
-H 'Accept: application/json'const res = await fetch("https://api-commerce.nucleoplatform.com/api/oms/v1/public/tracking/MTA0Mjo3ZjNjMmE5MS00YjZkLTRlMmEtOWMxZi0zZDhlNWEyYjZjNDA.Qm9Yc1Z3Tm1LcFJ0WmFHaEpk?lang=it", {
method: "GET",
headers: {
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-commerce.nucleoplatform.com/api/oms/v1/public/tracking/MTA0Mjo3ZjNjMmE5MS00YjZkLTRlMmEtOWMxZi0zZDhlNWEyYjZjNDA.Qm9Yc1Z3Tm1LcFJ0WmFHaEpk?lang=it', [
'headers' => [
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"brand": {
"name": "Acme Apparel",
"logo_url": "https://cdn.acme.example/logo.svg",
"accent_color": "#0F766E",
"support_email": "help@example.com",
"support_url": "https://shop.acme.example/pages/contact"
},
"locale": "it",
"order_name": "#1042",
"customer_first_name": "Giulia",
"destination": "Milano, IT",
"carrier": "DHL Express",
"tracking_numbers": [
"1234567890"
],
"carrier_url": "https://www.dhl.com/it-en/home/tracking.html?tracking-id=1234567890",
"status": "out_for_delivery",
"status_at": "2026-10-06T07:42:00+00:00",
"shipped_at": "2026-10-05T15:10:00+00:00",
"delivered_at": null,
"estimated_delivery_at": "2026-10-06T18:00:00+00:00",
"items": [
{
"title": "Organic Cotton Tee · M",
"quantity": 1
},
{
"title": "Fleece Hoodie · L",
"quantity": 2
}
],
"events": [
{
"occurred_at": "2026-10-06T07:42:00+00:00",
"status": "out_for_delivery",
"description": "Shipment is out with courier for delivery",
"location": "Milano, IT"
},
{
"occurred_at": "2026-10-05T21:03:00+00:00",
"status": "in_transit",
"description": "Processed at MILANO - ITALY",
"location": "Milano, IT"
},
{
"occurred_at": "2026-10-05T15:10:00+00:00",
"status": "picked_up",
"description": "Shipment picked up",
"location": "Bologna, IT"
}
]
}
}{
"error": "not_found"
}{
"message": "Too Many Attempts."
}Carrier webhooks
Inbound tracking events, one URL per carrier connection:
https://api-commerce.nucleoplatform.com/api/oms/v1/webhooks/tracking/{connection_id}.
The connection (Qapla', DHL or another carrier) and its secret are configured on the merchant's account under Settings › Orders › Channels and logistics; the connection ID is the UUID of that connection. Webhook secrets for carriers and Qapla' are set up with Nucleo during onboarding — ask Nucleo support for the URL and the secret.
Three payload formats are accepted, chosen by the connection type:
| Connection | Body | Authentication |
|---|---|---|
| Qapla' | Qapla' webhook (v1.2/1.3), one event | apiKey field in the body = the connection's API key |
| DHL | DHL Shipment Tracking – Unified (shipments[]) | ?key= query parameter = the connection's webhook secret |
| Any other carrier | Nucleo generic format (events[]) | X-Nucleo-Signature header = hex HMAC-SHA256 of the raw body with the connection's webhook secret |
/webhooks/tracking/{connection}Push tracking events for a carrier connection
Receives parcel events for the shipments of one carrier connection. The expected body and authentication depend on the connection type (see the table in the tag description):
- Generic carriers — body
{"events": [ … ]}(or a single event object), headerX-Nucleo-Signature: hex(HMAC-SHA256(raw body, webhook_secret)).statuscan be a Nucleo status, a carrier code translated by the connection's status map, or empty (Nucleo infers it fromdescription, in Italian, English, German, French or Spanish). - Qapla' — the Qapla' webhook body, authenticated by its
apiKeyfield (this scheme cannot be expressed as an OpenAPI security requirement). The reply is{"result":"OK"}as Qapla' expects. - DHL — the DHL Shipment Tracking – Unified body (
shipments[]), authenticated by?key=.
Matching. Events are matched to shipments by tracking_number (the most recent parcel with that
number). Events for unknown tracking numbers are ignored without error.
Idempotency. An event identical to one already stored (same shipment, tracking number, minute,
status and description) is ignored, so retries and overlapping deliveries are safe. applied counts
only new events.
Effects. The shipment takes the status of its most recent event. delivered marks the shipment
and order delivered (starting the return window) and closes open tracking anomalies;
exception, held and returned_to_sender open an anomaly for the merchant. Recent events may send
the merchant's customer notifications (out for delivery, delivered, ready for pickup, delivery
problem), each at most once.
Rate limit: 600 requests per minute per calling IP.
X-Nucleo-Signature header or key query parameterPath parameters
connectionstring (uuid)requiredID of the carrier or Qapla' connection in Nucleo.
Example:5f0c7e2a-8b3d-4c1e-9a6f-2d4b8e1c7a35
Request bodyapplication/json
One of the following shapes:
GenericWebhook
eventsarray of GenericEventrequiredAttributes of each item
tracking_numberstringrequiredParcel tracking number, as stored on the Nucleo shipment.
Example:0612345678901statusstringA Nucleo status (see
TrackingStatus), a carrier status code mapped by the connection's status map, or empty to let Nucleo infer it fromdescription.Example:delivereddescriptionstringEvent text (stored up to 500 characters).
Example:Delivered to recipientoccurred_atstring (date-time)When the event happened. Defaults to the time of receipt.
locationstringWhere it happened (stored up to 160 characters).
Example:Milanoexception_codestringDetail for
exceptionandheld.One ofaddress_wrongrecipient_absentrefuseddamagedcustomsheld_at_depotat_pickup_pointlostother
GenericEvent
tracking_numberstringrequiredParcel tracking number, as stored on the Nucleo shipment.
Example:0612345678901statusstringA Nucleo status (see
TrackingStatus), a carrier status code mapped by the connection's status map, or empty to let Nucleo infer it fromdescription.Example:delivereddescriptionstringEvent text (stored up to 500 characters).
Example:Delivered to recipientoccurred_atstring (date-time)When the event happened. Defaults to the time of receipt.
locationstringWhere it happened (stored up to 160 characters).
Example:Milanoexception_codestringDetail for
exceptionandheld.One ofaddress_wrongrecipient_absentrefuseddamagedcustomsheld_at_depotat_pickup_pointlostother
QaplaWebhook
apiKeystringrequiredThe Qapla' connection's API key in Nucleo.
trackingNumberstringrequireddatestringEvent date and time.
Example:2026-10-06 09:12:00qaplaStatusIDintegerrequiredqaplaStatusstringcourierStatusstringplacestringstatusDetailsarray of objectAttributes of each item
detailstring
courierstring
DhlWebhook
shipmentsarray of objectrequiredAttributes of each item
idstringrequiredDHL tracking number.
eventsarray of objectAttributes of each item
timestampstring (date-time)statusCodestringOne ofpre-transittransitdeliveredfailureunknowndescriptionstringlocationobjectChild attributes
addressobjectChild attributes
addressLocalitystringcountryCodestring
statusobjectLatest status, used when
eventsis empty (same shape as an event).
Responses
200Accepted.
appliedis the number of new events stored.application/jsonHeadersX-RateLimit-LimitRequests allowed per minute.X-RateLimit-RemainingRequests left in the current minute.
result"OK"requiredappliedintegerrequiredmin 0
400The body is not a JSON object.application/json
result"KO"requirederrorstringrequiredOne ofunknown connectioninvalid jsonunauthorized
401Wrong or missing signature / key /
apiKey.application/jsonresult"KO"requirederrorstringrequiredOne ofunknown connectioninvalid jsonunauthorized
404No carrier or Qapla' connection with this ID (
unknown connection). A non-UUID ID gets the generic route-not-found message. application/jsonOne of the following shapes:
WebhookError
result"KO"requirederrorstringrequiredOne ofunknown connectioninvalid jsonunauthorized
Message
messagestringrequired
429Too many requests from this IP.application/json
HeadersRetry-AfterSeconds to wait.X-RateLimit-LimitRequests allowed per minute.X-RateLimit-RemainingRequests left in the current minute.X-RateLimit-ResetUnix time when the limit resets.
messagestringrequired
curl -X POST https://api-commerce.nucleoplatform.com/api/oms/v1/webhooks/tracking/5f0c7e2a-8b3d-4c1e-9a6f-2d4b8e1c7a35 \
-H "X-Nucleo-Signature: $NUCLEO_SIGNATURE" \
-H 'Content-Type: application/json' \
-d '{
"events": [
{
"tracking_number": "0612345678901",
"status": "",
"description": "Consegnata al destinatario",
"occurred_at": "2026-10-06T11:05:00Z",
"location": "Milano"
},
{
"tracking_number": "0612345678902",
"status": "exception",
"exception_code": "recipient_absent",
"description": "Recipient not at home, second attempt tomorrow",
"occurred_at": "2026-10-06T11:20:00Z",
"location": "Torino"
}
]
}'const res = await fetch("https://api-commerce.nucleoplatform.com/api/oms/v1/webhooks/tracking/5f0c7e2a-8b3d-4c1e-9a6f-2d4b8e1c7a35", {
method: "POST",
headers: {
"X-Nucleo-Signature": `${process.env.NUCLEO_SIGNATURE}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"events": [
{
"tracking_number": "0612345678901",
"status": "",
"description": "Consegnata al destinatario",
"occurred_at": "2026-10-06T11:05:00Z",
"location": "Milano"
},
{
"tracking_number": "0612345678902",
"status": "exception",
"exception_code": "recipient_absent",
"description": "Recipient not at home, second attempt tomorrow",
"occurred_at": "2026-10-06T11:20:00Z",
"location": "Torino"
}
]
}),
});
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-commerce.nucleoplatform.com/api/oms/v1/webhooks/tracking/5f0c7e2a-8b3d-4c1e-9a6f-2d4b8e1c7a35', [
'headers' => [
'X-Nucleo-Signature' => getenv('NUCLEO_SIGNATURE'),
],
'json' => [
'events' => [
[
'tracking_number' => '0612345678901',
'status' => '',
'description' => 'Consegnata al destinatario',
'occurred_at' => '2026-10-06T11:05:00Z',
'location' => 'Milano',
],
[
'tracking_number' => '0612345678902',
'status' => 'exception',
'exception_code' => 'recipient_absent',
'description' => 'Recipient not at home, second attempt tomorrow',
'occurred_at' => '2026-10-06T11:20:00Z',
'location' => 'Torino',
],
],
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"result": "OK",
"applied": 2
}{
"result": "KO",
"error": "invalid json"
}{
"result": "KO",
"error": "unauthorized"
}{
"result": "KO",
"error": "unknown connection"
}{
"message": "Too Many Attempts."
}