← Plugin catalog
Developer Tools

Ship24 Tracking API

Ship24 Limited v1.0.0

Publisher description

From the marketplace listing

Ship24 shipment tracking for AI coding agents. Guides you through integrating the Ship24 Tracking API into your own project end to end: every endpoint and schema from the OpenAPI spec, webhook receivers versus polling, courier codes and required fields, tracking status mapping, the official Node SDK, and error and rate-limit troubleshooting. Your code calls Ship24 with your own API key.

Language: English · Automatically detected from descriptions.

Files & skills

File archives

Plugin package85 files · 219 KBBrowse files →
Skill instructions
ship24-couriers4.18 KB

View saved version →

---
name: ship24-couriers
description: Ship24 courier codes, courier auto-detection and per-courier required fields. Use when asked "which courier code for <carrier>", "force the courier", "does <carrier> need a postcode", "courier not detected", "courierCode", "list of couriers Ship24 supports", or when a tracker returns no results and the courier may be the cause. Explains when to send courierCode, the 3-per-request limit, requiredFields, deprecated codes, and how to fetch the courier list once and cache it instead of calling GET /couriers on every request.
license: MIT
---

# Courier codes and detection

Sources: [Couriers](https://docs.ship24.com/couriers) and the `GET /couriers` operation in the OpenAPI spec
(field table in `references/courier-fields.md`). The courier list itself is not bundled: it changes over time, and a
stale code is silently ignored by the API, so always read it from the live API.

## Auto-detection first

Ship24 detects the courier from the tracking number in most cases. `courierCode` is optional. Send it when:

- the user already knows the courier (it improves accuracy and avoids ambiguous matches);
- a tracker returned no results and the courier is a plausible cause;
- tracking must be restricted to specific couriers (`settings.restrictTrackingToCourierCode: true`).

Do not guess a code. If the courier is unknown, omit the field and let Ship24 detect it.

## Format and limits

| Rule | Value | Source |
| --- | --- | --- |
| Type | string or array of strings | OpenAPI `tracker-create-request` |
| Per request | up to 3 codes | Couriers page |
| Per shipment | up to 9 codes in total; `PATCH /trackers/{trackerId}` is the documented way to add codes afterward | Couriers and Trackers pages |
| Deprecated code (`isDeprecated: true`) | silently ignored, not rejected | Couriers page |
| `settings.restrictTrackingToCourierCode` | `true` pins tracking to the given codes only | OpenAPI |

`tracker_conflict` means a tracker with similar conflicting parameters already exists. Provide additional
parameters such as `shippingDate` or `destinationCountryCode` to differentiate it (see `ship24-troubleshooting`).

## Required fields per courier

Each courier entry carries `requiredFields`, a list of values that Ship24 needs to retrieve results for that
courier. Ship24 does not reject a request that omits them, but it may then fail to find the shipment.

| `requiredFields` value | Tracker field to send |
| --- | --- |
| `destinationPostCode` | `destinationPostCode` (1 to 32 chars) |
| `destinationCountryCode` | `destinationCountryCode` (ISO 3166-1 alpha-2 or alpha-3) |
| `courierAccount` | No matching request field exists in the spec today, so it cannot be supplied through the API. |

The dashboard CSV export exposes the same information as `is_destination_postcode_required`,
`is_destination_country_code_required`, `is_courier_account_required` columns.

## `courierCode` is not `sourceCode`

`courierCode` identifies the courier you ask Ship24 to track with (for example `us-post`). Event `sourceCode`
identifies the data source Ship24 obtained the event from (for example `usps-tracking`). They differ, and
source codes may evolve; never feed a `sourceCode` back as a `courierCode`.

## Getting the courier list

| Source | How | Notes |
| --- | --- | --- |
| Live API | `GET /couriers` or the `get_couriers` MCP tool | Full list, unpaginated, rate limit 1 request per second. Fetch once per session or deployment, cache it, never call it per request. |
| Dashboard | Integrations → Couriers, CSV download | Same data for humans. |

To answer "which code for <carrier>", fetch the list once and filter it locally on `courierName` and
`courierCode`; the response is large, so do not paste it whole into the conversation. Skip entries with
`isDeprecated: true`. `isPost` is `true` when the courier is a postal operator; `countryCode` is the courier's
main country and may be `null`.

Codes appearing in the OpenAPI examples: `us-post` (USPS), `fr-post` (La Poste), `palletways` (Palletways). For
any other courier, call the API; do not invent codes.

## Sibling skills

`ship24-integration` for the tracker creation flow, `ship24-troubleshooting` for `validation_error`,
`tracker_conflict` and "no results" triage.

Referenced files: 4

ship24-integration11.3 KB

View saved version →

---
name: ship24-integration
description: Integrate the Ship24 Tracking API into a codebase end to end. Use when asked to "add Ship24", "integrate Ship24", "integrate package, parcel or shipment tracking", create trackers, choose between webhooks and polling, or call the per-call tracking endpoint, in any language or framework. Detects the project's stack, confirms the Ship24 plan type and update mechanism, then guides implementation and verification against the OpenAPI spec. For webhook receivers, courier codes, statuses, errors or the Node SDK, the sibling ship24-* skills go deeper.
license: MIT
---

# Ship24 Tracking API integration

Opinionated wizard: detect the project, clarify the plan, guide the implementation, verify the result.

Facts in this skill come from [docs.ship24.com](https://docs.ship24.com) and the OpenAPI 3.1 spec at
[docs.ship24.com/assets/openapi/ship24-tracking-api.yaml](https://docs.ship24.com/assets/openapi/ship24-tracking-api.yaml).
The bundled `references/endpoints.md` and `schemas.md` are generated from that spec, `errors.md` and
`rate-limits.md` from the docs pages. When a detail matters (a field name, a status
code, a limit), read the reference file rather than guessing:

| Need | Read |
| --- | --- |
| Every endpoint, parameters, responses, rate limit | `references/endpoints.md` |
| Field tables for tracker, tracking, shipment, event, request bodies | `references/schemas.md` |
| HTTP statuses and error codes | `references/errors.md` |
| Rate limits and rate-limit headers | `references/rate-limits.md` |
| Webhook vs polling trade-offs, matching records, idempotent creation | `references/integration-patterns.md` |

Sibling skills: `ship24-webhooks` (build the receiver), `ship24-couriers` (courier codes and required fields),
`ship24-tracking-statuses` (map statuses to business logic), `ship24-troubleshooting` (errors), `ship24-node-sdk`
(the official `ship24` npm package).

## Phase 1: Detect

Scan before asking. Use the host tool's search if there is no shell.

```bash
grep -ri "ship24" . --include=*.{js,ts,py,rb,php,go,java,cs,env,json,yaml,yml} 2>/dev/null | head -20
grep -ri "SHIP24" .env .env.local .env.example 2>/dev/null
grep -rE "express|fastify|nest|fastapi|django|flask|rails|laravel|symfony|gin|spring|aspnet" \
  package.json requirements.txt pyproject.toml Gemfile composer.json go.mod pom.xml *.csproj 2>/dev/null | head
grep -rn "webhook" . --include=*.{js,ts,py,rb,php,go,java,cs} 2>/dev/null | head
grep -rE "bull|bullmq|celery|sidekiq|rabbitmq|sqs|pubsub|kafka" \
  package.json requirements.txt pyproject.toml Gemfile composer.json go.mod 2>/dev/null | head -5
```

Note: existing Ship24 usage (trackers created? webhook route present?), language and framework, HTTP client in
use, queue or async infrastructure, an existing public webhook route that can be extended.

If the project is Node.js or TypeScript, prefer the official SDK (`npm install ship24`) over hand-written HTTP
calls and switch to the `ship24-node-sdk` skill for the implementation details.

## Phase 2: Clarify

Ask only what detection could not determine, one question at a time.

1. **Account and key.** Does the user have a Ship24 account and API key? A free plan exists for integration and
   testing: [dashboard.ship24.com/onboarding](https://dashboard.ship24.com/onboarding). Keys live under
   Integrations → API Keys (up to 20 active keys per account).
2. **Plan type.** Per-shipment (the standard product: trackers, webhooks, polling) or per-call (a separate
   subscription, one synchronous endpoint, no trackers)? If unsure: per-shipment is the default choice; Ship24
   documents it as offering more features, faster fetching and lower overall cost.
3. **Update mechanism** (per-shipment only). Webhooks (Ship24 pushes updates, recommended) or polling (the app
   calls the API)? Webhooks need a public HTTPS endpoint. At thousands of trackers, polling stops being
   practical.
4. **Constraints.** New project or existing app? Serverless? No public URL? Multi-tenant?

Do not write code until plan type, key handling (env var) and update mechanism are settled.

## Phase 3: Guide

### Authentication and base URL (all plans)

- Base URL: `https://api.ship24.com/public/v1`
- Header on every request: `Authorization: Bearer <api key>` (keys look like `apik_...`).
- Read the key from the `SHIP24_API_KEY` environment variable. Never hardcode it, commit it, or ship it to a
  browser or mobile client: the key grants full access to the account's tracking data and quota.

```bash
# .env (never committed)
SHIP24_API_KEY=apik_your_key_here
```

### Route by plan

| Plan | Endpoints | Docs |
| --- | --- | --- |
| Per-shipment | `POST /trackers`, `POST /trackers/bulk`, `POST /trackers/track`, `GET /trackers/{trackerId}/results`, webhooks | [Trackers](https://docs.ship24.com/trackers), [Webhooks](https://docs.ship24.com/webhooks/overview) |
| Per-call | `POST /tracking/search` only | [Per-call API](https://docs.ship24.com/per-call-api) |

### Per-shipment plan

**Webhook flow (recommended)**

1. Build a `POST` endpoint that answers `2xx` quickly and processes asynchronously (see `ship24-webhooks`).
2. Register its URL in the dashboard: Integrations → Webhooks. There is no API for this.
3. Create a tracker per shipment with `POST /trackers` (or up to 100 at once with `POST /trackers/bulk`).
   Store the returned `trackerId`.
4. Ship24 pushes every new event to the endpoint. A webhook `events[]` array contains only the events
   discovered since the last push; API responses contain the full history.

**Polling flow**

1. `POST /trackers`, store `trackerId`.
2. Do not poll immediately: results are not available the instant a tracker is created.
3. `GET /trackers/{trackerId}/results` on an interval the app chooses. Ship24 does not mandate a cadence.
   Longer intervals for slower shipments.
4. Stop when the tracker's `isTracked` is `false`: Ship24 has stopped tracking that shipment.

**Synchronous one-shot flow**

`POST /trackers/track` creates the tracker and returns results in the same call. The first call can take up to
one minute because Ship24 queries couriers synchronously; some couriers do not support it and return results on
later calls. Calling it again with the same payload returns the same tracker with fresh results.

**Idempotency** (verified in Ship24's implementation, see the plugin's CONTRIBUTING). `POST /trackers` and
`POST /trackers/track` look up the account's trackers with the same `clientTrackerId` when one is sent, otherwise
with the same `trackingNumber`, and reuse a candidate without consuming quota when `shippingDate` (day
precision), `originCountryCode`, `destinationCountryCode`, `destinationPostCode`, `shipmentReference`,
`courierName`, `trackingUrl`, `settings.restrictTrackingToCourierCode` and the `courierCode` list all match.
Otherwise a new tracker is created. `tracker_conflict` comes back when the `clientTrackerId` belongs to an active
tracker with a different payload, and on `POST /trackers/track` when the `courierCode` list overlaps an existing
tracker's list without matching it; differentiate with `shippingDate` or `destinationCountryCode`, or resend the
same payload.

**Identify shipments.** Tracking numbers are not unique across couriers or time. Store Ship24's `trackerId`.
Alternatively set `clientTrackerId` to the app's own unique id: Ship24 validates its uniqueness across
subscribed trackers and most endpoints accept `?searchBy=clientTrackerId`. `shipmentReference` is free text,
not validated for uniqueness, and returned in responses and webhooks.

**Tracker creation fields** (full table in `references/schemas.md`, `tracker-create-request`):

| Field | Notes |
| --- | --- |
| `trackingNumber` | Required. 5 to 50 chars, `A-Z a-z 0-9 - _ / .`; returned uppercased, compare case-insensitively. Dummy values such as `123456789` are rejected. |
| `courierCode` | Optional string or array (max 3 per request). Improves detection; see `ship24-couriers`. |
| `clientTrackerId` | Optional, max 100 chars, unique across subscribed trackers. |
| `shipmentReference` | Optional, max 100 chars, not unique. |
| `originCountryCode`, `destinationCountryCode` | ISO 3166-1 alpha-2 or alpha-3 accepted; alpha-2 returned. Some couriers require the destination. |
| `destinationPostCode` | 1 to 32 chars. Some couriers require it. |
| `shippingDate` | ISO 8601. Rejected with `shipping_date_outdated` when older than 180 days. |
| `recipient.email`, `recipient.name` | Used for email notifications; `name` also feeds courier-restricted tracking data. Max 254 and 100 chars. |
| `settings.restrictTrackingToCourierCode` | Track only with the given courier codes. |

**Bulk creation.** `POST /trackers/bulk` accepts 1 to 100 items and answers `201` (the docs also list `200`),
`207` (partial) or an error. Its body is `{ status, summary, data, error }` with `status` in
`success | partial | error`, unlike the `{ data }` envelope of the other tracker endpoints. Rate limit 3 requests
per second.

### Per-call plan

`POST /tracking/search` with the tracking number (plus optional courier and destination hints) fetches results
synchronously from couriers. Response time can reach one minute. No tracker is created, nothing to store, no
webhooks. Requires an active per-call subscription; without one the API answers `no_active_subscription`.

### Data handling rules that bite

- Add-on fields (for example `shipment.delivery.aiPredictiveDeliveryDate`) are **absent**, not `null`, when
  the account lacks the option or Ship24 has no value. Check presence before reading.
- Event `occurrenceDatetime` is a `logistic-date-time`: local time, local time with offset, UTC, or date only.
  Keep it as a string; use the event `order` field to sort events that lack a time.
- `datetime`, `utcOffset`, `hasNoTime` (events) and `signedBy` (delivery) are deprecated. Do not build on them.
- Rate limits are per endpoint per second (most are 10 req/s, bulk 3, couriers and resend 1). Honor
  `Retry-After` on `429`. Details in `references/rate-limits.md`.
- Error bodies are `{ "errors": [{ "code", "message" }], "data": null }` (`message` optional), except the bulk
  endpoint's `{ status, summary, data, error }`. Branch on `code`, not on the message text. Codes in
  `references/errors.md`.

## Phase 4: Verify

- [ ] API key read from `SHIP24_API_KEY`; not in source, not in client-side code.
- [ ] `trackerId` (or a validated `clientTrackerId`) persisted for every created tracker.
- [ ] Webhook endpoint answers `2xx` before processing and matches payloads on `trackerId` or `clientTrackerId`.
- [ ] Duplicate deliveries handled: deduplicate on `events[].eventId`.
- [ ] Polling does not start immediately, uses an interval the team chose deliberately, and stops on
      `isTracked: false`.
- [ ] `401`, `403`, `422`, `429` (with `Retry-After`) and `5xx` handled; error `code` logged.
- [ ] Add-on fields read only when present.
- [ ] Tested end to end with a Ship24 sample tracking number such as `SHIP24_SAMPLE_DELIVERED_000`
      (see `ship24-tracking-statuses` for the full list).

## Key links

| Resource | URL |
| --- | --- |
| Dashboard, API keys, webhook URL | https://dashboard.ship24.com |
| Documentation | https://docs.ship24.com |
| API reference | https://docs.ship24.com/tracking-api-reference/ |
| OpenAPI 3.1 spec | https://docs.ship24.com/assets/openapi/ship24-tracking-api.yaml |
| Node SDK | https://github.com/ship24/ship24-node |
| Pricing | https://www.ship24.com/pricing |

Referenced files: 8

ship24-node-sdk10.1 KB

View saved version →

---
name: ship24-node-sdk
description: Use the official Ship24 npm package in TypeScript or Node.js. Triggers include "Ship24 Node SDK", "ship24 npm package", "use ship24", "ship24.trackers.create", "import ship24", "new Ship24", or asking to replace hand-written fetch or axios calls to Ship24 in Node/TypeScript. Routes deeper Node SDK questions away from generic integration guidance. Covers installation, constructor configuration, all 12 endpoint methods, error handling, per-call vs per-shipment differences, pagination, and when retries are needed.
license: MIT
---

# Ship24 Node SDK

Official SDK for the Ship24 Tracking API. Full TypeScript support, zero runtime dependencies, native `fetch`.

## Install

```bash
npm install ship24
```

The SDK README lists Node 18+, Bun, Deno, Cloudflare Workers and modern browsers as supported runtimes; CI tests Node 20/22/24, Bun and Deno. Never ship a production API key to a browser: keep the SDK on the server. It ships ESM and CommonJS:

```ts
// ESM
import { Ship24 } from 'ship24';
// CommonJS
const { Ship24 } = require('ship24');
```

## Quickstart

```ts
import { Ship24 } from 'ship24';

const apiKey = process.env.SHIP24_API_KEY;
if (!apiKey) throw new Error('SHIP24_API_KEY is not set');

const ship24 = new Ship24({ apiKey });
const tracker = await ship24.trackers.create({ trackingNumber: '1234567890' });
console.log(tracker.trackerId);
```

## Configuration

Pass a `Ship24Config` object to the constructor. Only `apiKey` is required:

```ts
type Ship24Config = {
  apiKey: string; // required; the SDK does not read the environment itself
  baseUrl?: string; // default 'https://api.ship24.com'; the SDK appends /public/v1
  timeoutMs?: number; // see Timeouts
  fetch?: typeof fetch; // custom fetch implementation
  headers?: Record<string, string>; // extra default headers; Authorization is always overwritten
};
```

### Timeouts

Defaults are 10 seconds per request and 60 seconds for the synchronous `ship24.trackers.track` and `ship24.perCall.track` (Ship24 queries couriers synchronously, up to about a minute). In SDK 1.0.0 the constructor's `timeoutMs` is stored but never read by the transport, so set the timeout per call:

```ts
const result = await ship24.trackers.track(
  { trackingNumber: '1234567890' },
  { timeoutMs: 120_000 }
);
```

Per-call options are accepted by every method: `{ timeoutMs?, signal?, headers? }`. Tracker-id methods also accept `{ searchBy: 'trackerId' | 'clientTrackerId' }` to resolve the id as your own client id.

## Methods

See `references/sdk-methods.md` for the full table of 12 methods, their endpoints, signatures and when each is used, and `references/endpoints.md` for the endpoint paths, response codes and rate limits generated from the OpenAPI spec. Highlights:

### Per-shipment (default, recommended)

Create trackers, receive updates via webhooks or polling, manage courier codes.

```ts
await ship24.trackers.create({ trackingNumber, courierCode?, clientTrackerId? });
await ship24.trackers.track({ trackingNumber });  // sync, ~60s
await ship24.trackers.bulkCreate([{ trackingNumber }, ...]);  // up to 100
await ship24.trackers.list({ page?, limit?, sort? });
await ship24.trackers.get(trackerId);
await ship24.trackers.getResults(trackerId);
await ship24.trackers.update(trackerId, { isSubscribed? });
await ship24.trackers.resendWebhooks(trackerId);
await ship24.couriers.list();  // rate limited 1 req/s
```

### Per-call (separate subscription)

Synchronous one-off lookups; no persistent tracker.

```ts
await ship24.perCall.track({ trackingNumber, destinationCountryCode? });
```

Returns `PerCallTracking[]`. Note there is **no `.tracker`** field, unlike `trackers.track`.

## Error handling

All errors extend `Ship24Error`. Catch by class, then optionally branch on `code`:

```ts
import {
  Ship24Error,
  NotFoundError,
  RateLimitError,
  SubscriptionError,
  AuthenticationError,
  ValidationError,
  ConflictError,
  QuotaError,
  ServerError,
  Ship24ConnectionError,
  Ship24TimeoutError,
} from 'ship24';

try {
  const tracker = await ship24.trackers.get('unknown-id');
} catch (err) {
  if (err instanceof NotFoundError) {
    console.error(`Tracker not found: ${err.code}`);
    // Handle 404
  } else if (err instanceof RateLimitError) {
    console.error(`Rate limited; wait ${err.retryAfter}s`, err.rateLimit);
    // err.rateLimit has { limit, remaining, reset }
  } else if (err instanceof SubscriptionError) {
    console.error(err.message); // Actionable: per-call subscription required?
  } else if (err instanceof ValidationError) {
    console.error(`Invalid request: ${err.code}`);
    // Handle 400 validation
  } else if (err instanceof ConflictError) {
    console.error(`Conflict: ${err.code}`); // tracker_conflict or request_conflict
  } else if (err instanceof QuotaError) {
    console.error(`Quota exceeded: ${err.code}`);
  } else if (err instanceof AuthenticationError) {
    console.error(`Auth failed: ${err.code}`); // Missing/invalid key or header
  } else if (err instanceof ServerError) {
    console.error(`Server error (5xx): ${err.httpStatus}`);
  } else if (err instanceof Ship24TimeoutError) {
    console.error(`Timed out after ${err.timeoutMs}ms`);
  } else if (err instanceof Ship24ConnectionError) {
    console.error(`Network error: ${err.message}`);
  } else if (err instanceof Ship24Error) {
    console.error(`Unexpected error: ${err.message}`);
  } else {
    throw err; // Not a Ship24 error
  }
}
```

`ValidationError` is an alias of `InvalidRequestError`, the name that appears in `err.name`. All `Ship24APIError` subclasses carry `httpStatus`, `code`, `errors[]`, `requestId` (from the `x-request-id` header), and `body` (raw response). Log the `requestId` when contacting support.

## Per-call vs per-shipment

Ship24 sells two products. The SDK keeps them explicit:

| Aspect | Per-shipment | Per-call |
| --- | --- | --- |
| Namespace | `ship24.trackers.*` | `ship24.perCall.*` |
| Usage counted | Shipments (trackers) | API calls |
| Tracker persists | Yes | No |
| Return type | `Tracking` (has `.tracker`) | `PerCallTracking` (no `.tracker`) |
| Subscription | Standard per-shipment plan | Separate "Per-call" subscription |

Calling `ship24.perCall.track` without an active per-call subscription throws `SubscriptionError` (HTTP 422) with a message pointing to the dashboard.

## Bulk creation

`trackers.bulkCreate` accepts 1 to 100 items and **never throws on the envelope**: HTTP 201/207/400/403 all resolve to a `BulkCreateResult`. Inspect the result's `status` and per-item `errors`:

```ts
const result = await ship24.trackers.bulkCreate([
  { trackingNumber: 'A' },
  { trackingNumber: 'B' },
]);

if (result.status === 'success') {
  console.log('All created');
} else if (result.status === 'partial') {
  // Some failed; iterate result.data to find which
  for (const item of result.data ?? []) {
    if (item.itemStatus === 'error') {
      console.error(item.inputData.trackingNumber, item.errors);
    }
  }
}
```

The SDK documents that HTTP 201, 207, 400 and 403 resolve to a `BulkCreateResult`; other failures throw the usual error classes.

## Honesty: what the SDK does not do

**No automatic retries or backoff.** The SDK raises on rate limit (429). You must handle it:

```ts
import { RateLimitError } from 'ship24';

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function retryOnRateLimit<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  try {
    return await fn();
  } catch (err) {
    if (err instanceof RateLimitError && attempts > 1) {
      await sleep((err.retryAfter ?? 1) * 1000);
      return retryOnRateLimit(fn, attempts - 1);
    }
    throw err;
  }
}
```

**No pagination helper.** `trackers.list` returns a flat array with no total count. Pagination is manual:

```ts
import type { Tracker } from 'ship24';

const allTrackers: Tracker[] = [];
let page = 1;
while (true) {
  const batch = await ship24.trackers.list({ page, limit: 100 });
  if (batch.length === 0) break;
  allTrackers.push(...batch);
  if (batch.length < 100) break;
  page++;
}
```

**No webhook helper.** Webhook verification and parsing are your responsibility. See the `ship24-webhooks` skill or `docs.ship24.com/webhooks/overview`. You must verify the `Authorization: Bearer <webhook-secret>` header yourself using constant-time comparison.

**Statuses are typed as `string`, not literal unions.** Compare against the documented snake_case values such as `"delivered"` or `"delivery_delivered"` (see `ship24-tracking-statuses`); the compiler will not catch a typo.

## Exported types

From `src/index.ts` and `src/types/*.ts`:

```ts
import type {
  // Domain
  Tracker, Shipment, ShipmentDelivery, ShipmentRecipient, TrackingEvent, Statistics,
  Courier, CourierRequiredField, WebhookMetadata, Tracking, PerCallTracking,
  LogisticDateTime, IsoDateTime,
  // Requests
  CreateTrackerRequest, UpdateTrackerRequest, ListTrackersParams, PerCallTrackRequest,
  TrackerRecipientInput, TrackerSettingsInput,
  // Responses
  ApiErrorItem, BulkCreateItem, BulkCreateResult, ResendWebhooksResult, WebhookHistoryEntry, WebhookHistory,
  // Config and errors
  Ship24Config, RequestOptions, TrackerLookupOptions, RateLimitInfo, Ship24APIErrorInit,
} from "ship24";
```

Value exports: `Ship24`, `TrackersResource`, `CouriersResource`, `PerCallResource`, `VERSION` and the error classes,
including `Ship24APIError` and `InvalidRequestError`.

The API object `Event` is exported as `TrackingEvent` to avoid shadowing the DOM global. `LogisticDateTime` stays a
plain string on purpose: coercing it to `Date` would lose the "no time" and offset information.

## Schema drift and weekly checks

The `ship24-node` repository runs a weekly workflow that re-downloads the official OpenAPI spec from `docs.ship24.com/assets/openapi/ship24-tracking-api.yaml` and opens a PR when the vendored copy changed. Reconciling the hand-written types with the new spec is a manual step in that PR's checklist, so a spec change is noticed weekly but the types are not diffed automatically.

## See also

For full integration patterns (when to use webhooks vs polling, idempotent creation, handling add-on fields), see the `ship24-integration` skill. For webhooks specifically, see `ship24-webhooks`.

Referenced files: 5

ship24-tracking-statuses6.22 KB

View saved version →

---
name: ship24-tracking-statuses
description: Map Ship24 tracking statuses to application logic using statusMilestone, statusCode and statusCategory. Use when asked "Ship24 status", "statusMilestone", "statusCode", "statusCategory", "is the parcel delivered", "terminal status", "notify the customer on status change", "sample tracking numbers", or when deciding which status field to store, filter or alert on. Covers the 8 milestones, 6 categories and 20 codes, event ordering, and the SHIP24_SAMPLE_* test numbers.
license: MIT
---

# Tracking statuses

Source: [Status](https://docs.ship24.com/status). The OpenAPI spec types these fields as plain strings and does
not encode the enums, so the docs table, mirrored in `references/statuses.md`, is the authoritative list.

## Three fields

| Field | Where | Meaning |
| --- | --- | --- |
| `statusMilestone` | every event, and at shipment level | Overall status of the shipment at that moment. Always present. |
| `statusCategory` | events | Category of the event (`data`, `transit`, `destination`, `customs`, `delivery`, `exception`). May be empty on non-significant events. |
| `statusCode` | events | Codified meaning of the event (20 values). May be empty on non-significant events. |
| `status` | events | Raw courier text, not normalized (for example `ENTREGADO. Su envío está entregado.`). Display it, never branch on it. |

For a high-level state use `shipment.statusMilestone`. For fine-grained triggers use `events[].statusCode`.

## Milestones

| `statusMilestone` | Label | Description (docs) | Typical handling |
| --- | --- | --- | --- |
| `pending` | Pending | The shipment doesn’t have events available yet or can’t be found. | Wait; if it persists, check the courier and required fields (`ship24-couriers`). |
| `info_received` | Info. Received | The shipment has been declared electronically and/or is in preparation by the shipper. | Show "label created". |
| `in_transit` | In Transit | The shipment has been accepted or picked up from the shipper and is on the way. | Default active state. |
| `out_for_delivery` | Out for Delivery | The shipment is about to be delivered, usually the same day. | Notify the recipient. |
| `failed_attempt` | Failed Attempt | A delivery attempt was made and failed (Recipient not available, business closed, etc.) | Notify; the courier usually retries or leaves the parcel at a pickup point. |
| `available_for_pickup` | To Pick Up | The shipment is ready to be picked up by the receiver. (At a pickup point such as a post office, a locker, or a local business) | Notify with urgency. |
| `delivered` | Delivered | The shipment has been delivered. (Delivered at home, picked up from a pickup point, etc.) | Close the flow. |
| `exception` | Exception | The shipment can’t be delivered due to issues that seem to be final (Returning, returned, lost, destroyed, etc.) | Open a support case. |

Ship24 documents no "terminal" flag. Trackers are disabled automatically after delivery, and `isTracked: false`
means tracking has stopped (delivery, inactivity or unsubscription). Stop polling on `isTracked`, not on a
status, and keep processing any webhook that still arrives.

## Categories and codes

Full table in `references/statuses.md`. Codes worth dedicated business rules:

| `statusCode` | Category | Meaning (docs) |
| --- | --- | --- |
| `delivery_delivered` | delivery | Shipment has been delivered. |
| `delivery_attempted` | delivery | Delivery attempted and unsuccessful. Usually, the delivery will be tried again the next day, or the shipment will be left at a pick-up point. |
| `delivery_available_for_pickup` | delivery | Shipment available for pickup at a pick-up point or at the Post Office. |
| `delivery_exception` | delivery | Issue during delivery or preventing delivery, which usually could be solved. |
| `delivery_refused` | delivery | Shipment refused by the recipient. |
| `customs_exception` | customs | Exception or delay during customs clearance. Additional documents or payment may be required. |
| `customs_rejected` | customs | Shipment rejected by customs. |
| `exception_return` | exception | Shipment undeliverable, will be or being returned. |
| `exception_lost` | exception | Shipment lost by the carrier. |
| `exception_discarded` | exception | Shipment destroyed by the carrier. |

## Timestamps and ordering

- `statistics.timestamps` holds the first-occurrence datetime of each milestone
  (`infoReceivedDatetime`, `inTransitDatetime`, `outForDeliveryDatetime`, `failedAttemptDatetime`,
  `availableForPickupDatetime`, `exceptionDatetime`, `deliveredDatetime`). Use it for timelines and durations.
- `events[].occurrenceDatetime` is a `logistic-date-time`: `2022-10-23T15:13:37` (courier local time),
  `2022-12-21T17:01:12+02:00`, `2022-09-03T23:58:12Z`, or a bare date `2022-08-14`. Store it as a string.
  When the time is missing, sort with the event `order` field (lower is older).
- API responses return the full event history; a webhook item carries exactly one event, and Ship24 does not
  guarantee delivery order. Compare `occurrenceDatetime` with the latest stored event before acting.
- `datetime`, `utcOffset` and `hasNoTime` on events are deprecated in favor of `occurrenceDatetime`;
  `signedBy` on `shipment.delivery` is deprecated too.

## Sample tracking numbers

Each sample simulates a set of events, with the corresponding `statusMilestone` updates, up to the desired
milestone. Change the last three digits to mint a new tracker with identical results, or reuse the exact number
with a unique `clientTrackerId`.

| `statusMilestone` | Sample |
| --- | --- |
| `pending` | `SHIP24_SAMPLE_PENDING_000` |
| `info_received` | `SHIP24_SAMPLE_INFO_RECEIVED_000` |
| `in_transit` | `SHIP24_SAMPLE_IN_TRANSIT_000` |
| `out_for_delivery` | `SHIP24_SAMPLE_OUT_FOR_DELIVERY_000` |
| `failed_attempt` | `SHIP24_SAMPLE_FAILED_ATTEMPT_000` |
| `available_for_pickup` | `SHIP24_SAMPLE_AVAILABLE_FOR_PICKUP_000` |
| `delivered` | `SHIP24_SAMPLE_DELIVERED_000` |
| `exception` | `SHIP24_SAMPLE_EXCEPTION_000` |

For integration and testing, the docs point to the free plan ([Getting started](https://docs.ship24.com/getting-started)).

## Sibling skills

`ship24-webhooks` for receiving events, `ship24-integration` for the overall flow, `ship24-troubleshooting`
when a status never moves past `pending`.

Referenced files: 4

ship24-troubleshooting7.36 KB

View saved version →

---
name: ship24-troubleshooting
description: Diagnose Ship24 Tracking API problems from the symptom. Use when asked about a Ship24 error, a 400, 401, 403, 422 or 429 response, tracker_not_found, parcel_not_found, tracker_conflict, quota_limit_reached, "no tracking results", "tracker not updating", "webhook not received", "rate limited", or when the Ship24 MCP tools return 401 or seem to be missing. Gives the cause and the fix for every documented HTTP status and error code.
license: MIT
---

# Troubleshooting

Sources: [Error management](https://docs.ship24.com/errors), [Rate limiter](https://docs.ship24.com/rate-limiter),
[Standard data format](https://docs.ship24.com/data-format), [Trackers](https://docs.ship24.com/trackers),
[Integrate with AI](https://docs.ship24.com/integrate-with-ai). Full tables: `references/errors.md`,
`references/rate-limits.md`, `references/statuses.md`, `references/endpoints.md`.

Error bodies are `{ "errors": [{ "code", "message" }], "data": null }`; `message` is optional. `POST /trackers/bulk`
is the exception: its body is `{ status, summary, data, error }` with the code under `error.code`. Branch on
`code`; `message` is for humans and may change.

## Symptom to fix

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| `401` | Missing or wrong `Authorization: Bearer apik_...` header, revoked key | Check the header and the key in Dashboard → Integrations → API Keys. |
| `403` | Insufficient permissions or endpoint limitations; on `POST /trackers/bulk` it carries `quota_limit_reached` | Check the subscription and quota in the dashboard. A missing plan for an endpoint answers `422` + `no_active_subscription` instead. |
| `400` + `validation_error` | A field failed validation | Read `message`. Country codes: ISO 3166-1 alpha-2 or alpha-3, uppercase. Tracking number: 5 to 50 chars, `A-Z a-z 0-9 - _ / .`, dummy values rejected. Postcode: 1 to 32 chars, `A-Z a-z 0-9 - _ / .` and space. |
| `400` + `tracker_conflict` | A tracker with similar conflicting parameters already exists: the `clientTrackerId` belongs to an active tracker with a different payload, or (on `POST /trackers/track`) the `courierCode` list overlaps an existing tracker's list without matching it | Provide additional parameters such as `shippingDate` and `destinationCountryCode` to differentiate the tracker, or resend the identical payload to reuse it. |
| `bulk_create_limit_exceeded` | More than 100 items in `POST /trackers/bulk` (bulk envelope, code under `error.code`) | Split into batches of 100. |
| `402` | Parameters valid but the request failed | Read the error `code`; it names the reason. |
| `404` + `tracker_not_found` | Unknown `trackerId`, or `clientTrackerId` used without `searchBy=clientTrackerId` | Check the id and the `searchBy` query parameter. |
| `409` + `request_conflict` | Same payload sent in parallel; one request won | Retry later; idempotent endpoints are safe to retry. |
| `400` + `shipping_date_outdated` | `shippingDate` older than 180 days | Ship24 does not track shipments older than 6 months. |
| `422` + `no_active_subscription` | No subscription covers the endpoint, most often `POST /tracking/search` without a per-call plan | Use the per-shipment endpoints (`POST /trackers/track` for a synchronous lookup) or subscribe. |
| `quota_limit_reached` | Trackers or calls above the plan's quota for the billing period | Wait for the period to reset or upgrade. Note that changed payloads create new trackers and consume quota. |
| `tracker_not_updatable` | The shipment has already been processed; courier and destination can only change while Ship24 has found no trace of it | Create a new tracker with the corrected data. |
| `parcel_not_found` | Shipment created recently and not yet visible at the courier, or too little information | Wait and retry; add `originCountryCode`, `destinationCountryCode`, `destinationPostCode`, `shippingDate` when known. Do not add made-up destination fields to sample numbers. |
| `processing_error` | Error while processing the data; on `POST /trackers/bulk` it means every item failed validation | Read each bulk item's `errors`. Otherwise retry once, then contact Ship24. |
| `webhook_url_missing` | The operation needs a webhook URL and none is configured | Set the URL in Dashboard → Integrations → Webhooks. |
| `429` | Rate limit exceeded | Wait `Retry-After` seconds (also `RateLimit-Reset`). Default limits are per endpoint per second: most endpoints 10, `POST /trackers/bulk` 3, `GET /couriers` and the resend endpoint 1. |
| `207` on bulk | Some items failed | Inspect each item in `data` and the `summary`; `status` is `partial`. |
| `500`, `502`, `503`, `504` | Ship24 side | Retry with exponential backoff. |

## Tracker created, no events

1. Tracking is not instant. Results appear once Ship24 has queried the couriers; wait before the first poll.
2. `isTracked: false` means Ship24 has stopped tracking this shipment; no new events will come.
3. Courier not detected: add `courierCode`, and the courier's `requiredFields` (`destinationPostCode`,
   `destinationCountryCode`; `courierAccount` has no request field). See `ship24-couriers`.
4. A deprecated `courierCode` is silently ignored. Check `isDeprecated` in `GET /couriers`.
5. `shipment.statusMilestone` stays `pending` when no events are available or the shipment cannot be found.

## Webhook not received

1. URL registered in Dashboard → Integrations → Webhooks? There is no API for it.
2. Public HTTPS endpoint reachable from the internet?
3. Does the endpoint answer `2xx` quickly? Ship24's implementation gives up after 15 seconds by default
   (verified in code, not yet documented); return `2xx` first and process afterwards.
4. Use the dashboard "test your integration" button, then `POST /trackers/{trackerId}/webhook-events/resend`
   (1 request per second) to replay a tracker's messages. `GET /trackers/{trackerId}/webhook-history/download`
   shows every push with response codes.
5. Filtering by source IP? The documented outgoing IP is `54.161.7.2`; the dashboard test button does not use it.

## Duplicate or out-of-order webhooks

Deliveries are retried when no `2xx` is received and the resend endpoint replays every message of a tracker,
so duplicates are normal. Deduplicate on `events[].eventId`. Order is not guaranteed: compare
`occurrenceDatetime` (and `order` when the time is missing) with the latest stored event.

## MCP tools

| Symptom | Cause | Fix |
| --- | --- | --- |
| Tools are listed but every call returns `401` | The server lists tools without a key (discovery lane) but needs `Authorization: Bearer` for calls | Set `SHIP24_API_KEY` where the host tool reads it (shell environment, plugin variable or setting), then reconnect. |
| `search_tracking` missing | The key's plan has no per-call subscription | Use `track` (per-shipment). Per-call-only keys see `search_tracking` and `get_couriers`; per-shipment-only keys see everything except `search_tracking`. |
| Tools did not change after upgrading the plan | Plan is detected once per session | Reconnect the MCP client. |
| `track` or `search_tracking` slow | Synchronous courier fetch | Up to one minute is documented and the MCP server aborts after 60 seconds; wait rather than retrying, and allow at least 60 seconds when calling the API directly. |

## Sibling skills

`ship24-webhooks` for the receiver contract, `ship24-couriers` for detection issues, `ship24-tracking-statuses`
for what a status means, `ship24-integration` for the overall flow.

Referenced files: 7

ship24-webhooks7.51 KB

View saved version →

---
name: ship24-webhooks
description: Build a correct Ship24 webhook receiver in any language. Use when asked to receive Ship24 tracking updates, build a webhook endpoint, verify webhook secrets, handle tracking or proof of delivery webhooks, or test webhook delivery. Covers authentication, payload structure, retry logic, ordering guarantees, and best practices to avoid data loss and duplicate processing.
license: MIT
---

# Building Ship24 webhook receivers

Ship24 pushes tracking events to your system via webhooks instead of requiring you to poll. Your webhook endpoint must validate the authentication, answer quickly, and process messages idempotently.

Facts in this skill come from [docs.ship24.com/webhooks](https://docs.ship24.com/webhooks/overview) and the Ship24 implementation verified on 2026-09-09. When implementing, consult:

| Need | Resource |
| --- | --- |
| Payload structure and field names | `references/webhook-payloads.md` (generated from the OpenAPI spec) and the example payloads under `assets/` |
| Event statuses and milestones | `ship24-tracking-statuses` skill |
| Error codes | `ship24-troubleshooting` skill |
| Receiver code examples | `references/receiver-checklist.md` (Express, FastAPI, Rails) |
| Testing your receiver | `ship24-webhook-test` skill |

Sibling skills: `ship24-integration` (full integration workflow), `ship24-webhook-test` (test your receiver), `ship24-tracking-statuses` (map status codes to business logic), `ship24-troubleshooting`.

## Configuration

**Webhook URL**: Dashboard only. Log in to [dashboard.ship24.com](https://dashboard.ship24.com), go to Integrations → Webhooks, and set your HTTPS endpoint. Optional: set a separate Proof of Delivery Webhook URL if you have the PoD add-on. The dashboard test button does NOT originate from the documented outgoing IP.

**Webhook secret (authentication)**: Each account has one shared secret sent as `Authorization: Bearer <secret>` in every request.

- It is NOT an HMAC signature; there is no per-message signature or replay protection.
- Compensate by using HTTPS, constant-time comparison of the secret (e.g., `crypto.timingSafeEqual`, `secrets.compare_digest`, `ActiveSupport::SecurityUtils.secure_compare`), whitelisting the outgoing IP `54.161.7.2`, and deduplicating on `metadata.messageId` and `events[].eventId`.

## Payload structure: tracking events

Ship24 sends tracking results under topic `tracking/events`:

```json
{
  "trackings": [{
    "metadata": {"generatedAt": "2025-03-04T17:13:35.000Z", "messageId": "...", "topic": "tracking/events"},
    "tracker": {"trackerId": "...", "trackingNumber": "...", "shipmentReference": "...", "clientTrackerId": "...", "isSubscribed": true, "createdAt": "..."},
    "shipment": {"shipmentId": "...", "statusMilestone": "delivered", "statusCode": "delivery_delivered"},
    "events": [{"eventId": "...", "status": "...", "occurrenceDatetime": "2025-03-04T17:12:57", "order": 9, "statusMilestone": "delivered"}],
    "statistics": {"timestamps": {...}}
  }]
}
```

**Structure:**
- Root key is `trackings` (array); each item has metadata, tracker (webhook-tracker, no `isTracked`/`courierCode`), shipment, events array (always exactly one event per webhook item), statistics.
- Match shipments on `tracker.trackerId` (preferred) or `tracker.clientTrackerId`.
- `metadata.topic` is `tracking/events` or `tracking/pod`.
- Add-on fields (e.g., `shipment.delivery.aiPredictiveDeliveryDate`) are absent, not `null`, when unavailable. Check field presence before reading.

## Payload structure: proof of delivery

Set a separate Proof of Delivery Webhook URL in the dashboard (if you have the PoD add-on). Payloads contain `metadata`, `tracker`, and `data` with fields: `status` (`found` or `unavailable`), `courier` (source code), `content.type` (text/plain, text/html, application/pdf, image/png, image/jpeg), `content.downloadUrl` (valid 7 days, download immediately). PoD payloads are never grouped; `trackings` always has exactly one item.

## Ordering and event deduplication

Webhooks are not ordered. Retries or out-of-order delivery can cause duplicates and older events to arrive late.

**Process:**
1. Deduplicate on `metadata.messageId` and `events[].eventId`.
2. Compare new event's `occurrenceDatetime` (a `logistic-date-time`: full datetime, datetime with offset, UTC datetime, or date only) to your latest stored event.
3. Use the `order` field (integer or null, lower is older) to break ties when `occurrenceDatetime` is equal or date-only.
4. Only apply business logic when the new event is newer than your latest stored event.

## Grouping and delays

By default, one event per webhook. The dashboard setting "Maximum number of updates per webhook message" controls it (default 1 on recent accounts; changing it requires contacting Ship24). A value above 1 introduces a delay of around 15 minutes so that updates can be grouped; `trackings` array length > 1, but each item still has exactly one event.

## Delivery contract

**Response:** Any `2xx` signals success. The docs say the body is ignored; Ship24's implementation treats a JSON body containing `success: false` as a failed delivery and retries it, so never return that. Answer quickly, then process asynchronously.

**Timeout:** Ship24 waits 15 seconds by default (verified in Ship24's implementation, not yet documented). No response = delivery fails.

**Retries:** the docs describe up to 20 retries with an exponential backoff "ranging from a few seconds to a few hours". Ship24's implementation, verified on 2026-09-09, makes up to 20 attempts in total: the first retry about 12 minutes after the failure, later attempts up to 12 hours apart, so deliveries can keep arriving for about six days. After the last attempt the message is not retried; use the resend endpoint to recover.

## Resending webhooks and history

**Resend:** `POST /public/v1/trackers/{trackerId}/webhook-events/resend` (rate limit 1 req/s, supports `?searchBy=clientTrackerId`). Resends ALL messages, so your receiver must be idempotent.

**History:** `GET /public/v1/trackers/{trackerId}/webhook-history/download` (supports `?searchBy=clientTrackerId`). Returns a JSON file with metadata and the log of sent pushes: request body, response, HTTP status, timestamps. Pending webhooks (not yet sent or failed) are excluded.

## Recommended receiver flow

1. Validate the secret (constant-time comparison).
2. Answer `2xx` immediately (persist or enqueue, then return).
3. Process asynchronously: parse JSON, iterate `trackings[]`, match on `tracker.trackerId` or `tracker.clientTrackerId`, skip if `metadata.messageId` or `events[].eventId` already seen, compare `occurrenceDatetime` to latest stored event, apply business logic.

See `references/receiver-checklist.md` for code examples (Express, FastAPI, Rails) and a complete checklist.

## Error codes

`webhook_url_missing`: Webhook URL is required but not configured. Set one in the dashboard.

## Testing without real parcels

Use sample tracking numbers: `SHIP24_SAMPLE_DELIVERED_000`, `SHIP24_SAMPLE_IN_TRANSIT_000`, etc. Change the last three digits to mint new trackers with the same events. See the `ship24-tracking-statuses` skill for the full list.

## Key links

| Resource | URL |
| --- | --- |
| Dashboard, webhook setup | https://dashboard.ship24.com/integrations/webhook/ |
| Webhook documentation | https://docs.ship24.com/webhooks/overview |
| Resend endpoint | [Ship24 API reference](https://docs.ship24.com/tracking-api-reference/#/operations/resend-webhooks) |
| Webhook history download | [Ship24 API reference](https://docs.ship24.com/tracking-api-reference/#/operations/download-webhook-history) |

Referenced files: 7

ship24-webhook-test7.63 KB

View saved version →

---
name: ship24-webhook-test
description: Test and debug a Ship24 webhook receiver end to end. Use when asked to test your webhook, simulate tracking updates, replay webhook messages, run a local webhook receiver, or validate webhook handling. Provides a runnable HTTP server and walkthrough for creating sample shipments, triggering events, and auditing delivery.
license: MIT
---

# Testing Ship24 webhooks end to end

Verify your receiver handles tracking updates correctly before going to production.

## Quick start

### 1. Start a local receiver

From the installed skill directory (wherever your tool placed `ship24-webhook-test`), or from a clone of https://github.com/ship24/ship24-ai-plugin:

```bash
cd skills/ship24-webhook-test
node scripts/receiver.mjs
```

Output:

```
Ship24 webhook test receiver listening on http://localhost:3000 (POST any path)
secret check: off (set SHIP24_WEBHOOK_SECRET to enable); log: stdout
```

The receiver:
- Accepts `POST` on any path.
- Responds `200 { ok: true }` immediately.
- Logs to stdout (or a file if `LOG_FILE` is set).
- Validates the `Authorization: Bearer <secret>` header if `SHIP24_WEBHOOK_SECRET` is set.
- No external dependencies; uses Node 22 built-in `node:http`.

**Env variables:**

- `PORT`: Listen port (default 3000).
- `SHIP24_WEBHOOK_SECRET`: Optional. When set, rejects requests whose `Authorization` header does not match using a constant-time comparison.
- `LOG_FILE`: Optional. Append NDJSON logs to a file instead of stdout.

### 2. Expose to the internet

Ship24 must reach your receiver. Use any tunnel you already have; examples:

- **ngrok**: `ngrok http 3000`
- **Cloudflare Tunnel**: `cloudflared tunnel --url http://localhost:3000`
- **LocalTunnel**: `npx localtunnel --port 3000`

Note the public HTTPS URL the tunnel prints.

### 3. Configure the dashboard

1. Log in to [dashboard.ship24.com](https://dashboard.ship24.com)
2. Go to Integrations → Webhooks
3. Paste the public URL into the Webhook URL field
4. Copy the Webhook Secret and set it locally:

```bash
export SHIP24_WEBHOOK_SECRET=your_webhook_secret
```

5. (Optional) Test using the dashboard button. This test does not originate from the documented outgoing IP `54.161.7.2`, so it is blocked if you restrict by IP.

### 4. Create a sample tracker

Use one of the bundled MCP tools, the Node SDK, or a direct HTTP call:

**Via MCP (if available):**

```
create_tracker trackingNumber=SHIP24_SAMPLE_DELIVERED_000
```

**Via Node SDK:**

```javascript
import { Ship24 } from 'ship24';

const client = new Ship24({ apiKey: process.env.SHIP24_API_KEY });
const tracker = await client.trackers.create({
  trackingNumber: 'SHIP24_SAMPLE_DELIVERED_000'
});
console.log('Tracker:', tracker.trackerId);
```

**Via curl:**

```bash
curl -X POST https://api.ship24.com/public/v1/trackers \
  -H "Authorization: Bearer $SHIP24_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"trackingNumber": "SHIP24_SAMPLE_DELIVERED_000"}'
```

Sample tracking numbers:

- `SHIP24_SAMPLE_DELIVERED_000` - Delivered
- `SHIP24_SAMPLE_IN_TRANSIT_000` - In transit
- `SHIP24_SAMPLE_EXCEPTION_000` - Exception
- Change the last three digits to mint a fresh tracker with the same event sequence (e.g., `SHIP24_SAMPLE_DELIVERED_123`).

### 5. Watch deliveries

As you create the tracker, Ship24 discovers events and sends webhooks. Check your receiver logs:

```
{"timestamp":"2025-03-04T17:13:02.114Z","method":"POST","path":"/","authorization":"Bearer [masked]","topic":"tracking/events","messageId":"...","trackerId":"...","trackingNumber":"SHIP24_SAMPLE_DELIVERED_000","statusMilestone":"delivered","eventId":"...","statusCode":"delivery_delivered","occurrenceDatetime":"2025-03-04T17:12:57"}
```

### 6. Replay with resend

To replay all webhooks for a tracker (useful if you dropped messages):

**Via MCP:**

```
resend_webhooks trackerId=<trackerId>
```

**Via curl:**

```bash
curl -X POST https://api.ship24.com/public/v1/trackers/<trackerId>/webhook-events/resend \
  -H "Authorization: Bearer $SHIP24_API_KEY"
```

The receiver logs again. It is your responsibility to deduplicate on `metadata.messageId` and `events[].eventId`.

### 7. Audit with webhook history

Download the full delivery history for a tracker:

**Via MCP:**

```
download_webhook_history trackerId=<trackerId>
```

**Via curl:**

```bash
curl -X GET "https://api.ship24.com/public/v1/trackers/<trackerId>/webhook-history/download" \
  -H "Authorization: Bearer $SHIP24_API_KEY" \
  -o webhook-history.json
```

Returns metadata and a log of every sent webhook delivery (pending ones are excluded): request body, response status, response headers, and timestamps.

### 8. Test offline with curl

To test your receiver without creating a real tracker:

```bash
curl -X POST http://localhost:3000 \
  -H "Authorization: Bearer your_webhook_secret" \
  -H "Content-Type: application/json" \
  -d @assets/webhook-tracking-events.example.json
```

The example file is a sample tracking webhook payload. Your receiver should log the event.

## Receiver behavior

The bundled `scripts/receiver.mjs`:

- **Accepts `POST` on any path**: `/`, `/webhooks`, `/tracking`, etc. all work.
- **Responds immediately**: reads the body, answers 200 with `{ ok: true }`, then logs.
- **Validates secret**: If `SHIP24_WEBHOOK_SECRET` is set, rejects requests without a matching `Authorization: Bearer <secret>` header (401). Comparison is constant-time to mitigate timing attacks.
- **Rejects malformed JSON**: non-JSON bodies receive 400 and are still logged; bodies over 1 MB receive 413 and the connection is closed.
- **Logs to stdout or file**: Each webhook logged as a single-line JSON object (NDJSON).
- **Logs fields of interest**: timestamp, method, path, authorization scheme (the secret is masked), topic, messageId, trackerId, clientTrackerId, trackingNumber, statusMilestone, each event's eventId, statusCode and occurrenceDatetime; for proof-of-delivery messages, the PoD status and content type.
- **No external dependencies**: Uses only Node 22 built-in modules.

## Testing checklist

Before going to production:

- [ ] Receiver logs incoming webhooks without errors.
- [ ] Secret validation works: requests without the correct header are rejected.
- [ ] Response is sent before async processing.
- [ ] Sample tracker triggers webhook delivery once Ship24 has fetched its events (not instant).
- [ ] Resend endpoint replays old messages (receiver must dedupe).
- [ ] Webhook history download works and shows delivery attempts and responses.
- [ ] Multiple shipments (with different statuses) all arrive correctly.
- [ ] Receiver handles the IP allowlist (`54.161.7.2`) if you restrict by IP at production.
- [ ] Offline curl test with example JSON parses without errors.

## Next steps

After confirming delivery:

1. Deploy your receiver to production (public HTTPS URL).
2. Update the dashboard webhook URL.
3. Implement deduplication on `metadata.messageId` and `events[].eventId`.
4. Implement ordering: compare `occurrenceDatetime` to the latest stored event.
5. Add async processing: return 200, then apply business logic (update shipment, send notifications, etc.).
6. Set up logging and monitoring for webhook failures.
7. Keep the resend endpoint and the webhook history download in your debugging toolbox.

See the `ship24-webhooks` skill for full implementation details, payload structure, and best practices.

## Key links

| Resource | URL |
| --- | --- |
| Webhook documentation | https://docs.ship24.com/webhooks/overview |
| Resend endpoint | https://docs.ship24.com/tracking-api-reference/#/operations/resend-webhooks |
| Webhook history | https://docs.ship24.com/tracking-api-reference/#/operations/download-webhook-history |
| Sample tracking numbers | Ship24 Tracking Statuses skill |

Referenced files: 7

Package details

Publisher declarations from the archived package. These are separate from our research and the live service's terms.

Package license
MIT
Package author
Ship24
Keywords
ship24, tracking, shipment-tracking, package-tracking, parcel-tracking, courier, logistics, mcp, agent-skills

Package observed Oct 2, 2026.

Technical details
First seen
Sep 30, 2026 · 22:02 UTC
Last seen
Oct 2, 2026 · 12:00 UTC
Collection status
Collected

plugins_6aace9b25cc8819185620a8407053ddb

Download plugin data (JSON)