{
"event_type": "parcels.event.created",
"data": {
"parcel": {
"parcel_id": 12345678,
"tracking_number": "3SYZXG132912330",
"carrier_code": "dpd",
"announced_at": "2026-01-15T10:00:00Z",
"order_number": "ORDER-12345",
"source": "shipping",
"source_id": "412345678"
},
"event": {
"id": 734512001,
"event_type": "carrier",
"timestamp": "2026-01-15T10:30:00Z",
"phase": "in_transit",
"exception": "none",
"status_type": "success",
"sub_status": "none",
"description": "Parcel is on its way",
"end_of_tracking": false,
"is_offered": false,
"is_carrier_responsible": null,
"is_returned": false,
"location_details": {
"city": "Eindhoven",
"country": "NL",
"house_number": null,
"postal_code": null,
"street": null,
"service_point_id": null,
"depot": null
},
"extra_data": null
}
}
}
Event payloads
Parcel event created
Delivered to your connection endpoint when a parcel tracking event occurs (e.g., a status change such as shipped, delivered, or returned).
Subscribe to this event type using parcels.event.created when creating a subscription.
WEBHOOK
ParcelEventCreated
{
"event_type": "parcels.event.created",
"data": {
"parcel": {
"parcel_id": 12345678,
"tracking_number": "3SYZXG132912330",
"carrier_code": "dpd",
"announced_at": "2026-01-15T10:00:00Z",
"order_number": "ORDER-12345",
"source": "shipping",
"source_id": "412345678"
},
"event": {
"id": 734512001,
"event_type": "carrier",
"timestamp": "2026-01-15T10:30:00Z",
"phase": "in_transit",
"exception": "none",
"status_type": "success",
"sub_status": "none",
"description": "Parcel is on its way",
"end_of_tracking": false,
"is_offered": false,
"is_carrier_responsible": null,
"is_returned": false,
"location_details": {
"city": "Eindhoven",
"country": "NL",
"house_number": null,
"postal_code": null,
"street": null,
"service_point_id": null,
"depot": null
},
"extra_data": null
}
}
}
A single subscription to
parcels.event.created delivers two unrelated response shapes, selected by data.event.event_type:
event.event_type | Response shape | What it represents |
|---|---|---|
carrier, calculated, internal | Tracking Event | A parcel tracking update: phase/exception/status_type/sub_status plus location and carrier metadata. |
claim | Claim Event | A Support Automation ticket update: its own phase/sub_status values, ticket/category identifiers, and refund/resolution fields. |
The two shapes do not share fields beyond
event_type and timestamp — in particular, phase and sub_status are different enums on each shape. Always check event.event_type first and parse the rest of event according to that value; do not assume a tracking field (e.g. exception) is present on a claim event, or vice versa.Parsing the payload
Whatever language you use, the approach is the same:- Read
data.event.event_type. - Pick the model:
carrier,calculatedorinternal→Tracking Event;claim→Claim Event. - Parse
data.eventwith that model. Treatphase,sub_status,category,resolutionandoriginas plain strings, because new values can be added at any time.
from typing import Annotated, Literal, Union
from pydantic import BaseModel, Field
class TrackingEvent(BaseModel):
event_type: Literal["carrier", "calculated", "internal"]
timestamp: str
phase: str
sub_status: str
# ...other tracking fields
class ClaimEvent(BaseModel):
event_type: Literal["claim"]
timestamp: str
phase: str
sub_status: str
ticket_id: int
claim_event_id: int
# ...other claim fields
class EventData(BaseModel):
event: Annotated[Union[TrackingEvent, ClaimEvent], Field(discriminator="event_type")]
import { z } from "zod";
const TrackingEvent = z.object({
event_type: z.enum(["carrier", "calculated", "internal"]),
timestamp: z.string(),
phase: z.string(),
sub_status: z.string(),
// ...other tracking fields
});
const ClaimEvent = z.object({
event_type: z.literal("claim"),
timestamp: z.string(),
phase: z.string(),
sub_status: z.string(),
ticket_id: z.number().int(),
claim_event_id: z.number().int(),
// ...other claim fields
});
const EventData = z.object({
event: z.discriminatedUnion("event_type", [
TrackingEvent.extend({ event_type: z.literal("carrier") }),
TrackingEvent.extend({ event_type: z.literal("calculated") }),
TrackingEvent.extend({ event_type: z.literal("internal") }),
ClaimEvent,
]),
});
<?php
$event = $payload['data']['event'];
switch ($event['event_type']) {
case 'carrier':
case 'calculated':
case 'internal':
handleTrackingEvent($event); // phase, exception, status_type, sub_status, ...
break;
case 'claim':
handleClaimEvent($event); // phase, sub_status, ticket_id, claim_event_id, ...
break;
default:
// Unknown event type: acknowledge with a 2xx and ignore it.
break;
}
Identifying the parcel
data.parcel.parcel_id: the Sendcloud tracking parcel ID. It is not the Shipping API parcel ID.data.parcel.source_id: the parcel ID in the source system. The value depends ondata.parcel.source:shipping: the Shipping API parcel ID.external_parcel: the ID you sent when you registered the parcel with the Parcel Tracking API.
- Do not use
source_idas a unique key. When a carrier uses a tracking number again, two parcels can have the samesource_id.
Internal events
- Events with
event_typeinternalcome from Sendcloud, not from the carrier. - Examples: announcing, announced, ready to send, cancelled.
- An internal event can arrive before the
parcel.createdevent for the same parcel.
Events sent again
Sendcloud can send the same carrier event more than one time.event.idstays the same for each copy. For internal and calculated events,event.idisnull.- The data can change between copies. When you get an
event.idthat you already have, replace the earlier copy. - Example: a carrier sends a status code that Sendcloud does not know yet. Sendcloud sends the event with an unknown status. When Sendcloud maps the status code, it sends the event again with the correct status. Sendcloud does this only for carrier events from the last 7 days.
Body
application/json
Event data delivered to your endpoint
Response
200
Return a 2xx status to acknowledge receipt of the event.