DataShift Logo

DataShift Freight API · v0.3.0

API Reference

Carrier-agnostic freight orchestration. Rate-shop and book shipments across all your configured carrier accounts from a single API.

Base URL: https://datashift.com.au/api/freightAuth: Authorization: Bearer dsfrt_<64-hex-chars>

Connectors

get/connectorslistConnectors

List active connectors

Returns all active carrier connectors for the authenticated tenant. Use the `id` values as `connectorId` in rate and booking requests. Credentials are never included in the response.

Responses

200Active connectors for this tenant
ConnectorListResponse
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
429Too many requests for this API key — see Retry-After header
Error

Rates

post/ratesgetAllRates

Get rates from all active connectors

Fans out to every active carrier connector for the authenticated tenant and returns all available services. Uses `Promise.allSettled` — one carrier being down does not fail the request; its `error` entry is included in the response instead.

Request body

RateRequest

Responses

200Rate results from all connectors. Each entry is either a successful carrier result or an error — check `success` to distinguish.
AggregatedRateResponse
400Invalid request — validation failed or carrier rejected input
Error
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
429Too many requests for this API key — see Retry-After header
Error
post/{connectorId}/ratesgetConnectorRates

Get rates from a specific connector

Queries a single carrier connector. Set `serviceCode` to get pricing for one service only; omit it to receive all available services.

Parameters

NameInRequiredDescription
connectorIdpath✓Database ID of the carrier connector account (from `GET /connectors`)

Request body

RateRequest

Responses

200Available rated services for this connector
SingleConnectorRateResponse
400Invalid request — validation failed or carrier rejected input
Error
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
404Connector not found or not active for this tenant
Error
429Too many requests for this API key — see Retry-After header
Error
502Carrier API returned an error or is unavailable
Error

Shipments

post/{connectorId}/shipmentscreateShipment

Create a shipment

Submits a booking to the carrier. On success, returns an `orderNumber` and `consignmentNumber` (if assigned immediately by the carrier). **Collection time rules (TNT AU):** `collectionDateTime` must be 07:00–17:00, `collectionCloseTime` must be 13:00–19:00, and the window between them must be ≥ 120 minutes — all evaluated as wall-clock time local to the `collection` address, not a fixed national zone (a WA collection is checked against WA local time, not AEST). Violations return 400 with a structured `errors[]` list.

Parameters

NameInRequiredDescription
connectorIdpath✓Database ID of the carrier connector account (from `GET /connectors`)

Responses

201Booking created successfully
BookingResponse
400Invalid request — validation failed, the carrier rejected the input, or `rateId` could not be linked (`code: QUOTE_NOT_FOUND`: the rate does not exist for this tenant, connector and `serviceCode`, or does not belong to the supplied `quoteId`). Nothing is booked at the carrier when `QUOTE_NOT_FOUND` is returned.
Error
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
404Connector not found or not active for this tenant
Error
410`code: QUOTE_EXPIRED` — the quote that `rateId` belongs to is more than 48 hours old and can no longer be linked to a booking. Nothing is booked at the carrier; request fresh rates and book with the new `rateId`.
Error
429Too many requests for this API key — see Retry-After header
Error
502Carrier API returned an error or is unavailable
Error
get/shipmentslistShipments

List shipments

Returns shipments for the authenticated tenant, sorted by `createdAt` (most recent first unless `sortDirection=asc`). Poll this endpoint to detect when `consignmentNumber` becomes available for carriers that assign it asynchronously. **Pagination:** `offset` is an exact row skip — `offset=30&limit=50` returns rows 31–80 of the filtered, sorted result. `total` counts every shipment matching the filters. The response echoes the effective `limit` and `offset`. **Filters** combine with AND. `createdFrom` is inclusive and `createdTo` exclusive; a bare date (`YYYY-MM-DD`) means 00:00:00Z that day. **Scope:** requires `bookings:read` (implied by `bookings:write`). Invalid query parameters return 400 with `errors[]` naming each field.

Parameters

NameInRequiredDescription
limitquery—Number of shipments to return (1–200, default 50). Non-numeric values are rejected with 400.
offsetquery—Exact number of shipments to skip (default 0). Non-numeric values are rejected with 400.
statusquery—Only shipments with this status
carrierquery—Comma-separated carriers, e.g. `TNT_AU,TGE_AU`
sourcequery—Only shipments booked via the API or the portal
createdFromquery—Inclusive lower bound on `createdAt`. ISO 8601 date (`YYYY-MM-DD`, meaning 00:00:00Z) or date-time with `Z` or an offset.
createdToquery—Exclusive upper bound on `createdAt`. Same formats as `createdFrom`; must be after it.
deliveredquery—`true` returns only shipments with a recorded `deliveredAt`. `false` is not supported yet (400) — omit the parameter to include undelivered shipments.
customerReferencequery—Exact match on `customerReference` (max 100 characters)
searchquery—Case-insensitive substring match across order number, consignment number, carrier booking reference, customer reference, receiver company, destination suburb and destination postcode (max 200 characters)
sortDirectionquery—Sort direction on `createdAt` (default `desc`)

Responses

200Shipment list
ShipmentListResponse
400Invalid query parameter
Error
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
429Too many requests for this API key — see Retry-After header
Error
get/shipments/{id}getShipment

Get shipment detail

Returns full detail for a single shipment including sender, collection, and receiver addresses, item dimensions, and booking metadata. Requires `bookings:read` (implied by `bookings:write`).

Parameters

NameInRequiredDescription
idpath✓Datashift shipment ID (from booking response or `GET /shipments`)

Responses

200Shipment detail
ShipmentDetailResponse
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
404Connector not found or not active for this tenant
Error
429Too many requests for this API key — see Retry-After header
Error
get/tracktrackByConsignment

Track by consignment number

Returns live tracking information for a shipment identified by its carrier consignment number. **Lookup strategy:** 1. If `carrier` is given, queries that carrier's connector directly — no shipment lookup, no fan-out. Use this when you already know the carrier (e.g. from your own shipment record) for a faster, unambiguous result. 2. Otherwise, searches your booked shipments for a matching `consignmentNumber`. If found, queries that carrier directly. 3. If no match in your shipments and no `carrier` given, fans out to all active carrier connectors and returns the first non-null result. **Carrier support:** TNT AU, BEX AU, and TGE AU all support tracking. TNT AU and TGE AU return proof-of-delivery (`pod`) on a delivered shipment (TGE: `signedBy` only) — see the `TrackingInfo` schema. **Polling:** Some carriers (BEX AU) process consignments asynchronously after booking. If tracking returns `supported: true` but `tracking: null`, wait 30–60 seconds and retry.

Parameters

NameInRequiredDescription
consignmentquery✓Carrier consignment number to look up (e.g. the `consignmentNumber` from a booking response)
carrierquery—Optional. Scopes the lookup to a single carrier, skipping the shipment-match and fan-out steps. Recommended when you already know the carrier.

Responses

200Tracking result. Check `supported` — if false the carrier does not expose a tracking API.
TrackingResponse
400Missing `consignment` query parameter, or `carrier` is not a recognized carrier
Error
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
404Consignment not found in your shipments and no active carrier returned tracking data, or (with `carrier` given) no active connector exists for that carrier
Error
429Too many requests for this API key — see Retry-After header
Error
get/shipments/{id}/labelgetShipmentLabel

Get shipment label

Returns the PDF shipping label for a booked shipment. **Label availability:** Most carriers (including TNT AU) generate the label at booking time. If the label is not yet available, this endpoint returns 404 with `code: LABEL_NOT_AVAILABLE` — poll with exponential backoff until it becomes available. **Response format:** `application/pdf`. Save as a file or pipe directly to a label printer.

Parameters

NameInRequiredDescription
idpath✓Datashift shipment ID (from booking response or `GET /shipments`)

Responses

200PDF label
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
404Shipment not found, or label not yet available from the carrier. Check `code` — `LABEL_NOT_AVAILABLE` means retry; anything else means the shipment ID is wrong.
LabelNotAvailableError
409Shipment has been cancelled in DataShift — no label is served for a cancelled shipment.
Error
429Too many requests for this API key — see Retry-After header
Error
post/shipments/cancel/eligibilitycheckShipmentCancelEligibility

Check shipment cancel eligibility

**DataShift-only cancellation — read this first.** None of DataShift's carriers (TNT AU, BEX AU (Border Express) and TGE AU) expose a cancel API today. Cancelling a shipment through DataShift only marks it cancelled in DataShift — the carrier is never called or notified, and `carrierNotified` in every response from this API is always `false`. If the label has already been printed and handed to the carrier (or is likely to be, e.g. it's on a manifest or a pickup has been booked), destroy it — cancelling here does not stop the carrier from collecting or invoicing the freight. Pure read — this endpoint never cancels anything. Call it before `POST /shipments/cancel` to show the caller what will happen: which ids are blocked (and why) and which will succeed with a warning. **Scope:** requires `bookings:read` or `bookings:cancel`. **Blocking reasons** (`results[].code` when `eligible: false`): `NOT_FOUND`, `ALREADY_CANCELLED`, `DELIVERED`, `IN_CARRIER_NETWORK` (collected, tracking-scanned, or already in transit / out for delivery / exception). **Warnings** (`results[].warnings[].code`, stack independently, only on an eligible result): `MANIFESTED` (already sent to the carrier on a manifest — expect a charge; cancelling anyway requires `acknowledgeManifestedIds` containing its id, or `acknowledgeManifested: true`, on the actual cancel call), `PICKUP_BOOKED` (a pickup is booked; informational only, some carriers auto-book pickups so this doesn't block). An id that doesn't belong to your tenant (or doesn't exist at all) comes back exactly like any other blocked id — `{ eligible: false, code: 'NOT_FOUND' }` — never a 404, so this can't be used to probe whether an id exists in another tenant.

Responses

200Per-id eligibility, in the same (deduped) order as the request
CancelEligibilityResponse
400Invalid request — validation failed or carrier rejected input
Error
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
429Too many requests for this API key — see Retry-After header
Error
post/shipments/cancelcancelShipments

Cancel shipments (DataShift only)

**DataShift-only cancellation.** No carrier is called or notified — see `POST /shipments/cancel/eligibility` above for the full caveat. `carrierNotified` is always `false`. Requires the `bookings:cancel` scope specifically; `bookings:write` alone does not grant it. Runs eligibility fresh for every id (not trusting a stale client-side check). A manifested shipment is skipped with `MANIFEST_ACK_REQUIRED` unless its id is in `acknowledgeManifestedIds` or `acknowledgeManifested: true` is given. Prefer `acknowledgeManifestedIds`: it covers only the shipments you checked, so one manifested after your eligibility check is skipped rather than cancelled unseen. **Required acknowledgement:** `acknowledgeCarrierNotNotified` must be exactly `true`. Missing or `false` returns 422 `CARRIER_ACK_REQUIRED` — distinct from a generic validation 400 so you can prompt specifically for that acknowledgement. **Response:** `cancelled` (ids actually cancelled), `skipped` (ids that weren't, each with a `code` — the same blocking codes as the eligibility endpoint, plus `MANIFEST_ACK_REQUIRED`), `carrierNotified: false`, and `warning` (always present, the DataShift-only caveat as text — surface it to the end user). `manifestedWarning` is present only when at least one cancelled shipment had already been manifested.

Responses

200Cancel outcome — check `skipped` for any ids that were not cancelled
CancelShipmentsResponse
400Invalid request — validation failed or carrier rejected input
Error
401Missing or invalid API key
Error
403Freight access not active for this tenant, or the API key lacks the `bookings:cancel` scope (note: `bookings:write` alone is not sufficient)
Error
422Missing or false `acknowledgeCarrierNotNotified`
Error
429Too many requests for this API key — see Retry-After header
Error

Pickups

post/{connectorId}/pickupscreatePickupBooking

Book a pickup

Books a pickup — either derived from an existing consignment (`shipmentId`, address/items locked to that consignment) or standalone (`collection`, `items`, and `receiver` supplied directly, all three required together). A partial standalone submission (e.g. `collection` without `items`/`receiver`) is rejected with a 400.

Parameters

NameInRequiredDescription
connectorIdpath✓Database ID of the carrier connector account (from `GET /connectors`)

Responses

201Pickup booked successfully (or recorded as failed — check `status`)
PickupBookingResponse
400Invalid request — validation failed or carrier rejected input
Error
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
404Connector not found or not active for this tenant
Error
429Too many requests for this API key — see Retry-After header
Error
500The carrier accepted the pickup booking, but DataShift failed to record it locally — the pickup is real at the carrier and must not be retried (a retry would book a second real pickup). Reconcile manually using the returned `orderNumber` (the carrier's own pickup reference).
{
  message?: string
  orderNumber?: string
}
502Carrier API returned an error or is unavailable
Error
get/pickups/{id}getPickupBooking

Retrieve a pickup booking

Parameters

NameInRequiredDescription
idpath✓

Responses

200Pickup booking detail
PickupBookingDetailResponse
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
404Connector not found or not active for this tenant
Error
429Too many requests for this API key — see Retry-After header
Error

Printing

get/print-deviceslistPrintDevices

List print devices

Returns active (non-revoked) Datashift print devices registered for the tenant, each with its full list of reported printers. Use the `id` of the desired device as `deviceId` when calling `POST /shipments/{id}/print`. Optionally use one of that device's `printers[].id` as `printerId` to override which physical printer on the device receives the label — omit it to use the device's preferred printer (`isPreferred: true`). Devices are registered from the Datashift portal (Settings → Freight → Print Devices). Each device runs the Datashift label client software and listens for print jobs over Pub/Sub. Revoked devices are excluded from this list. A device that hasn't reported its printer list yet (older label-client versions) has an empty `printers` array.

Responses

200Active print devices for this tenant
PrintDeviceListResponse
401Missing or invalid API key
Error
403Freight access is not active for this tenant, or the API key lacks the scope this endpoint requires
Error
429Too many requests for this API key — see Retry-After header
Error
post/shipments/{id}/printprintShipmentLabel

Print shipment label

Fetches the shipment label from the carrier and queues a print job to the specified device. **Prerequisites:** - Direct print must be enabled for the tenant in Settings → Freight. - The target device must be registered, active, and running the Datashift label client. **Typical flow:** Call `GET /print-devices` once during setup, store each device's `id` (and, if you need per-job printer overrides, its relevant `printers[].id`) in your WMS/ERP config keyed by workstation, then pass the relevant IDs in this request after booking. **Handling `DEVICE_NOT_FOUND`:** If the response is 404 with `code: DEVICE_NOT_FOUND`, the stored device ID is stale (device was revoked or replaced). Re-fetch `GET /print-devices`, overwrite your stored device list, and prompt the user to select a device before retrying. **Handling `LABEL_NOT_AVAILABLE`:** The label is not yet ready from the carrier — poll `GET /shipments/{id}/label` with exponential backoff and retry once available.

Parameters

NameInRequiredDescription
idpath✓Datashift shipment ID (from booking response or `GET /shipments`)

Request body

PrintRequest

Responses

201Print job queued successfully
PrintJobCreated
400Validation failed — missing deviceId, or printerId does not belong to the specified device
Error
401Missing or invalid API key
Error
403Freight access not active, or direct print not enabled for this tenant
Error
404Shipment or device not found, or label not yet available
PrintNotFoundError
409Shipment has been cancelled in DataShift — no label is queued for print for a cancelled shipment.
Error
429Too many requests for this API key — see Retry-After header
Error

Schemas

ConnectorSummary

{
  id: string  // Use as `connectorId` in rate and booking requests
  carrier: TNT_AU | BEX_AU | TGE_AU | NLN_AU
  name: string  // Human-readable label for this connector
  autoBookPickup: boolean  // Whether this connector places a carrier pickup automatically after a successful consignment. A connector with a standing/recurring physical pickup arrangement already in place with its carrier will have this off — no pickup should be booked against it at all, whether automatically by DataShift or by a caller's own separate pickup-booking call. Cache this per connector (e.g. when syncing connectors) rather than re-checking it per shipment.
  services: {
    serviceCode: string
    description: string
  }[]  // Available services for this connector. All three carriers (TNT AU, BEX AU, TGE AU) currently expose a static service catalog, so this is populated immediately — before any connection test — falling back to the static catalog until a successful test discovers account-specific active services instead. Only returns an empty array for a carrier with neither a static catalog nor a successful test on record.
}

ConnectorListResponse

{
  connectors: ConnectorSummary[]
}

Address

{
  suburb: string
  postcode: string
  state: string
  residential: boolean  // Whether this is a residential address. Triggers a residential surcharge on collection (RSP) or delivery (RSD) where applicable.
}

BookingAddress

{
  companyName: string
  contactName?: string
  phoneAreaCode?: string
  phoneNumber?: string
  address1: string
  address2?: string
  suburb: string
  state: string
  postcode: string
  email?: string  // Contact email for this party. Used by all three carriers as the contact email on the relevant carrier document (e.g. TNT AU's address block, BEX AU's ContactEmail). TGE AU specifically requires an email on `collection` — falls back to the connector's configured fallback address if omitted, but booking fails if neither is present.
}

Package

{
  count: integer
  lengthCm: number
  widthCm: number
  heightCm: number
  weightKg: number
}

RateRequest

{
  from: Address
  to: Address
  packages: Package[]
  dangerousGoods: boolean
  date?: string  // Shipping date (YYYY-MM-DD). Defaults to today in `timezone` if supplied, otherwise Australia/Sydney.
  localDateTime?: string  // Current local time (yyyy-MM-ddTHH:mm:ss, no TZ suffix). Used by TNT to determine booking cutoff eligibility. Defaults to the current time in `timezone` if supplied, otherwise Australia/Sydney.
  serviceCode?: string | null  // If set, returns pricing for this service only. Omit (or pass null) to receive all available services.
  isDocument?: boolean  // Mark as a document shipment (TNT AU — affects packaging type and commodity code)
  businessUnit?: IPEC | PRIORITY_AU  // Selects which TGE AU business unit config (account code / rate card) to use. Ignored by carriers other than TGE AU.
  timezone?: string  // IANA timezone of the collection location, used by TNT AU to default `date`/`localDateTime` when they are not supplied. Must be one of the seven distinct AU state/territory IANA zones (e.g. `Australia/Sydney`, `Australia/Perth`) — any other value is ignored and the server falls back to its own resolved timezone. Ignored by carriers other than TNT AU.
}

RatedService

{
  serviceCode: string
  description: string
  price: number  // Ex-GST price in major units. `priceExGstMinor` carries the exact value in cents.
  currency: string
  eta?: string | null  // Carrier ETA as sent: datetime (TNT), date (BEX) or null (TGE). Deprecated; use `etaDate` / `etaAt`.
  transitDays?: integer | null  // Business days from dispatch to ETA, for every carrier; see `transitDaysSource`.
  bookingCutoff: string  // Latest time to book this service today, as the carrier sent it (`HH:mm:ss`); `''` is the legacy "no cutoff" value. Use `bookingCutoffTime`.
  collectionCutoff: string  // Latest time for courier collection today, as the carrier sent it (`HH:mm:ss`); `''` is the legacy "no cutoff" value. Use `collectionCutoffTime`.
  priceGst?: number  // GST component of the price in major units, for every AUD rate: sent by the carrier or derived at 10% (see `gstSource`).
  priceIncGst?: number  // Total price including GST in major units, for every AUD rate.
  visibility?: shown | hidden  // "hidden" services are ones the tenant has chosen to de-prioritise in Settings — still returned, but callers may want to group/collapse them the way the portal does. Services switched fully "off" never appear here at all.
  rateId?: string  // Pass this back as `rateId` (and optionally the response's top-level `quoteId`) on `POST /{connectorId}/shipments` to link the booking to this specific rate. DataShift verifies it belongs to your tenant and matches the connector/serviceCode being booked, then populates the shipment's recorded price/ETA/transit from this rate rather than requiring you to resend them. Rejected if the rate's quote is more than 48 hours old. Optional — omit it and pass the price/eta/transitDays fields directly if you already track your own pricing.
  priceExGstMinor: integer  // Ex-GST price in minor units (cents). Exact; prefer it to the float `price`.
  gstMinor: integer | null  // GST in minor units, for every AUD rate: sent by the carrier or derived at 10% (see `gstSource`); 0 for a GST-free service. Null only for a non-AUD price.
  priceIncGstMinor: integer | null  // Inc-GST price in minor units; always `priceExGstMinor + gstMinor`.
  gstSource: CARRIER | DERIVED | CLIENT  // Where `gstMinor` came from: `CARRIER` (sent by the carrier), `DERIVED` (10% of the ex-GST price, rounded half-up) or `CLIENT`.
  taxTreatment: TAXABLE_GST | GST_FREE | UNKNOWN  // `TAXABLE_GST`, `GST_FREE`, or `UNKNOWN` (a non-AUD price).
  etaDate: string | null  // Estimated delivery date at the destination (`YYYY-MM-DD`, no timezone), for every carrier; TGE AU's is derived from its transit time (see `etaSource`). Null when no ETA is available.
  etaByLocalTime: string | null  // Deliver-by time on `etaDate` in the destination's local time (`HH:mm`), when the service has one (for example 17:00, or 09:00 for a 9:00 service).
  etaSource: CARRIER_SERVICE | CARRIER_LANE | DERIVED_FROM_TRANSIT | CARRIER_TRACKING | CLIENT  // Where the ETA came from: `CARRIER_SERVICE` (the carrier's ETA for this service), `CARRIER_LANE` (one ETA for every service on the lane, BEX AU), `DERIVED_FROM_TRANSIT` (dispatch date plus business days), `CARRIER_TRACKING` or `CLIENT` (sent by the caller).
  etaAt: string | null  // The ETA as an instant when `etaByLocalTime` is known, or null for a date-only ETA; never take its date part as the ETA date (a 09:00 AEST ETA is 23:00Z the previous day).
  transitCalendarDays: integer | null  // Calendar days from dispatch to ETA, for reference; `transitDays` counts business days.
  transitDaysSource: CARRIER | DERIVED | CLIENT  // Where `transitDays` came from: `CARRIER` (the carrier sent a duration), `DERIVED` (business days from dispatch to the carrier's ETA) or `CLIENT`.
  service: {
    key: string
    name: string
    tier: SATCHEL | EXPRESS | ROAD | PALLET
    businessUnit: IPEC | PRIORITY_AU
  }  // The catalogue entry for this service. `key` is `CARRIER:serviceCode`; `tier` and `businessUnit` are null when unknown.
  bookingCutoffTime: string | null  // Latest local time (`HH:mm`, collection state) to book this service for its dispatch date. Null when the carrier states none.
  collectionCutoffTime: string | null  // Latest local time (`HH:mm`, collection state) for collection on the dispatch date. Null when the carrier states none.
  expiresAt: string | null  // When this rate stops being bookable by `rateId` (48 hours after the rate-shop).
  issues: GST_ROUNDING_ADJUSTED | MONEY_INCONSISTENT | QUOTE_SNAPSHOT_INCONSISTENT | SERVICE_NOT_CATALOGUED | ETA_LANE_LEVEL | ETA_UNAVAILABLE | ETA_UNPARSEABLE | ETA_BEFORE_DISPATCH | ETA_ON_NON_BUSINESS_DAY | TRANSIT_ZERO_DAYS | TRANSIT_FRACTIONAL | TRANSIT_UOM_CONVERTED | TRANSIT_ETA_DISAGREE | DISPATCH_ROLLED | CALENDAR_OUT_OF_RANGE | LEGACY_NO_QUOTE | LEGACY_ETA_UNRECOGNISED_TIME | TRACKING_TIMESTAMP_UNPARSEABLE | TRACKING_STATUS_UNMAPPED | PAYER_ACCOUNT_MISSING | COLLECTION_DATE_DIFFERS_FROM_QUOTE | STATE_UNKNOWN | POSTCODE_INVALID[]  // Data-quality notes on this rate (for example `ETA_LANE_LEVEL`, `STATE_UNKNOWN`). Informational: the rate is still bookable.
}

AggregatedRateResult

{
  connectorId: string
  carrier: string
  success: boolean
  rates?: RatedService[]  // Present when success is true
  error?: CarrierError  // Present when success is false
}

AggregatedRateResponse

{
  quoteId: string  // Identifies this rate-shop call as a whole — every rate across every carrier in `results` was requested together under this id. Not the same as any individual rate's own `rateId`. Returned even when the tenant has no active connectors, in which case `results` is empty.
  results: AggregatedRateResult[]
}

SingleConnectorRateResponse

{
  quoteId: string  // Identifies this rate-shop call as a whole — every rate in `rates` was requested together under this id.
  rates: RatedService[]
}

Items

{
  count: integer
  totalWeightKg: number
  maxLengthCm: number
  maxWidthCm: number
  maxHeightCm: number
}

BookingRequest

{
  sender: BookingAddress
  collection: BookingAddress  // Collection address — often the same as sender but can differ (e.g. a warehouse pickup for an office-based account).
  receiver: BookingAddress
  items: Items
  packagingCode: CT | BX | SA | PA | EN | CR | DR | RO | BG | PC  // Packaging type: `CT` — Carton, `BX` — Box, `SA` — Satchel, `PA` — Pallet, `EN` — Envelope, `CR` — Crate, `DR` — Drum, `RO` — Roll, `BG` — Bag, `PC` — Piece. Sent to TNT AU as `PackagingCode`; TGE AU uses it to derive the shipment item description (it has no packaging-code field of its own).
  serviceCode: string  // Carrier service code from a prior `getRates` call
  collectionDateTime: string  // Pickup ready time, in wall-clock time local to the `collection` address (no TZ suffix — TNT AU's own rule is "in the Collection Location", never a fixed national zone). On TNT AU must be 07:00–17:00, ≥ 120 minutes before collectionCloseTime.
  collectionCloseTime: string  // Premises close time, local to the `collection` address, as an hhmm string. On TNT AU must be 13:00–19:00.
  payer: {
    type: Sender | Receiver | ThirdParty  // `ThirdParty` is rejected on TNT AU connectors — see `TNT_THIRD_PARTY_UNSUPPORTED` above.
    accountNumber?: string  // The account to bill. Required when `type` is `Receiver` or `ThirdParty` (see `PAYER_ACCOUNT_REQUIRED` above). Not required when `type` is `Sender` (the connector's own account is charged by default) — but on TNT AU, supplying one while `type` is `Sender` will still charge that account, so leave it blank unless that's intended.
    subAccount?: string  // TGE AU only (SubAccountCode) — disambiguates per-state accounts. Ignored by TNT AU and Border Express.
    thirdPartyAddress?: BookingAddress  // TNT AU only. The third-party account holder's address. Only meaningful when `type` is `ThirdParty` — but note TNT AU itself rejects `ThirdParty` bookings (see `TNT_THIRD_PARTY_UNSUPPORTED` above), so this is effectively unused for TNT AU today. BEX AU and TGE AU don't have a distinct third-party address concept and ignore it.
  }  // Who is billed for this shipment.

**Validation rules** (returned as 400 `errors[]` entries with these `code` values):
- `PAYER_ACCOUNT_REQUIRED` — `accountNumber` is required when `type` is `Receiver` or `ThirdParty` (all carriers).
- `TNT_THIRD_PARTY_UNSUPPORTED` — TNT AU has no third-party billing concept; `type: ThirdParty` is rejected on TNT AU connectors. Bill the receiver and supply their TNT account number instead.
  businessUnit?: IPEC | PRIORITY_AU  // Selects which TGE AU business unit config (account code / ShipmentID range) to use. Ignored by carriers other than TGE AU.
  dangerousGoods: boolean
  dangerousGoodsDetails?: {
    unNumber: string  // UN number (4 digits)
    packingGroup?: I | II | III  // Packing group (if applicable)
    hazClass: string  // Hazard class (e.g. `3`, `8`)
    packageType: string  // Package type description
    quantity: integer
    aggregateQuantity: integer
    shippingName: string  // Proper shipping name
    subRisk?: string  // Subsidiary risk (optional)
    contactName: string
    contactPhone: string
  }  // Required when `dangerousGoods` is true.
  customerReference?: string
  specialInstructions?: string
  alertAddresses?: {
    email: string
    phone?: string  // Mobile number for SMS alerts
    alertTypes: string[]  // Alert event types to subscribe to
  }[]  // TNT AU — email/SMS notifications for shipment events.
  keepInStore?: {
    numberOfDays: integer  // Number of business days to hold (1–5)
  }  // TNT AU — hold shipment at depot rather than attempting delivery.
  pickupBookingNumber?: string  // TNT AU — pre-arranged pickup booking reference
  extendedWarranty?: {
    value: number  // Declared value in AUD (max $10,000)
    class: A | B | C  // Warranty class tier
  }  // TNT AU — declare shipment value for extended warranty coverage.
  foodFreight?: boolean  // TNT AU — mark shipment as food freight
  bexAlerts?: {
    alertType: DESPATCHED | DELIVERY_STATUS | DELIVERED
    contactName: string
    emailAddress: string
  }[]  // BEX AU — email notifications for shipment events.
  timeSensitive?: {
    timeslot?: {
      dateTime: string  // Appointment datetime (ISO 8601)
      reference: string  // Appointment booking reference
    }  // Specific delivery appointment
    deliveryWindow?: {
      startDate: string
      endDate: string
      startTime?: string  // Earliest acceptable delivery time (HH:MM)
      endTime?: string  // Latest acceptable delivery time (HH:MM)
    }  // Date range delivery window
  }  // BEX AU — request a time-sensitive delivery. Provide either `timeslot` (specific appointment) or `deliveryWindow` (date range with optional time bounds).
  pallets?: {
    chep?: {
      exchangeIn?: integer
      exchangeOut?: integer
      directTransferToRec?: integer
      transferOffBex?: integer
      docketNumber?: string
    }
    loscam?: {
      exchangeIn?: integer
      exchangeOut?: integer
      directTransferToRec?: integer
      transferOffBex?: integer
      docketNumber?: string
    }
  }  // BEX AU — pallet exchange configuration for CHEP and/or LOSCAM.
  customerBarcodes?: string[]  // BEX AU — customer-supplied barcodes to associate with the consignment (max 50 chars each)
  rateId?: string  // Links this booking to one specific offered rate from a prior `getRates` call (that rate's `rateId` field — renamed from `quoteId` on the rate object; see the breaking-change note below). DataShift verifies it belongs to your tenant and matches the connector/serviceCode being booked, then uses its price/ETA/transit rather than the raw `quoted*` fields below. Rejected if the rate's quote is more than 48 hours old. Optional: omitting it (including a caller still sending the pre-rename `quoteId` field name, which is no longer read here) is treated exactly like never having rate-shopped through DataShift at all — the booking proceeds using the raw `quoted*` fields below, unverified. This is intentional, not an error: it's the same fallback bookings without any rate-shop call have always used. **Breaking change:** before this field existed, booking requests sent this same link as `quoteId`. That name now means something else entirely (see below) — a caller still sending the old `quoteId` field to link a rate will have it silently ignored rather than rejected, and should switch to `rateId`.
  quoteId?: string  // The rate-shop call `rateId` came from (the response's top-level `quoteId`) — a different value from the old, pre-rename meaning of `quoteId` on a booking request (which used to mean what `rateId` means now). Optional, and only checked when `rateId` is also supplied — if both are given and they don't actually belong together, the booking is rejected.
  quotedPriceExGstMinor?: integer  // Ex-GST price of the quote this was booked from, in minor currency units (cents). Only used when `rateId` is not supplied — informational, recorded as reported, not independently verified.
  quotedGstMinor?: integer  // GST component, in minor units. Only used when `rateId` is not supplied; when omitted for an AUD price it is derived at 10% (recorded as `quotedGstSource: DERIVED`).
  quotedPriceIncGstMinor?: integer  // Inc-GST price, in minor units. Only used when `rateId` is not supplied; when omitted for an AUD price it is `quotedPriceExGstMinor + quotedGstMinor`.
  quotedCurrency?: string
  committedEta?: string  // The quote's ETA, echoed back at booking time: the carrier ETA string (as in `RatedService.eta`) or a `YYYY-MM-DD` date (as in `RatedService.etaDate`). Only used when `rateId` is not supplied.
  quotedTransitDays?: integer  // The quote's `transitDays` (business days), echoed back at booking time. Only used when `rateId` is not supplied.
  bookingCutoff?: string  // The quote's `bookingCutoff`, echoed back at booking time.
  collectionCutoff?: string  // The quote's `collectionCutoff`, echoed back at booking time.
}

ConsignmentBookingRequest

BookingRequest & {
  packages: Package[]
}

BookingResponse

{
  orderNumber: string  // Booking reference issued by the carrier.
  consignmentNumber?: string | null  // Consignment/tracking number. Present immediately for some carriers (e.g. TNT AU); null for carriers that issue it asynchronously.
  barcodes?: string[]  // Carrier-issued barcode numbers for individual items in the consignment (BEX AU only)
  shipmentId: string  // Datashift shipment ID — use with `GET /shipments/{id}`, `GET /shipments/{id}/label`, and `POST /shipments/{id}/print`
  connectorId: string
  pickupStatus?: NOT_BOOKED | BOOKED | FAILED | RECONCILE_NEEDED  // Outcome of the connector's auto-book-pickup setting, if enabled. `NOT_BOOKED` if auto-booking is off — call POST /pickups to book one explicitly. `FAILED` means no pickup exists at the carrier — safe to retry. `RECONCILE_NEEDED` means the carrier accepted the pickup but DataShift failed to save the record — a real pickup exists, do NOT retry (that would book a second one); `labelWarning` carries the carrier's pickup reference to reconcile manually. This response's own `orderNumber` field above is NOT updated to reflect it, and `RECONCILE_NEEDED` is only ever reported here — `GET /shipments/{id}`'s `pickupStatus` has no record to compute it from and will read `NOT_BOOKED` instead, though its `orderNumber` may pick up the carrier reference later if a best-effort background sync succeeds.
  labelWarning?: string  // Present when the pickup booking failed or needs reconciliation (see pickupStatus) or the carrier returned a non-fatal warning alongside the label.
}

PickupBookingRequest

{
  shipmentId?: string  // Derived mode — book against this existing consignment. Omit for standalone.
  pickupDate: string
  readyTime: string
  closeTime: string
  serviceCode?: string  // Required for standalone (non-derived) pickups on TNT AU and TGE AU connectors — both reject a booking with no service code. Ignored by BEX AU. In derived mode (`shipmentId` given), the original shipment's service code is used automatically and this field is optional for every carrier.
  collection?: BookingAddress  // Standalone mode only — required together with items and receiver
  items?: Items  // Standalone mode only — required together with collection and receiver
  receiver?: BookingAddress  // Standalone mode only — required together with collection and items
}

PickupBookingResponse

{
  pickupBookingId?: string
  orderNumber?: string | null
  status?: BOOKED | FAILED
  connectorId?: string
}

PickupBookingDetail

{
  id?: string
  tenantId?: string
  connectorId?: string
  carrier?: TNT_AU | BEX_AU | TGE_AU | NLN_AU
  status?: BOOKED | FAILED
  orderNumber?: string | null  // Carrier pickup reference. Null when status is FAILED.
  shipmentId?: string | null  // Null for a standalone pickup not derived from an existing consignment.
  pickupDate?: string
  readyTime?: string
  closeTime?: string
  serviceCode?: string | null  // Required for standalone TNT AU and TGE AU pickups; ignored by BEX AU. Populated automatically in derived mode.
  collection?: BookingAddress
  items?: Items
  lastErrorCode?: string | null
  lastErrorMessage?: string | null
  source?: API | PORTAL
  createdByUserId?: string | null  // Null for API-created bookings.
  createdAt?: string
}

PickupBookingDetailResponse

{
  pickup: PickupBookingDetail
}

ShipmentSummary

{
  id: string  // Datashift shipment ID
  carrier: TNT_AU | BEX_AU | TGE_AU | NLN_AU
  connectorName: string
  orderNumber: string  // Legacy: the carrier pickup reference once a pickup is booked, otherwise the carrier's order or consignment reference; use `pickupReference` / `carrierOrderNumber` for the two values separately.
  consignmentNumber?: string | null  // Null if not yet assigned by the carrier
  carrierBookingRef?: string | null  // Datashift-generated booking reference handed to the carrier at booking time, for carriers whose own booking API doesn't return a usable reference of its own (currently only TGE AU). Null for other carriers.
  customerReference?: string | null
  status: CONSIGNED | CANCELLED
  pickupStatus: NOT_BOOKED | BOOKED | FAILED  // In one rare case, a pickup the carrier actually accepted can show as `NOT_BOOKED` here (not `FAILED`) because DataShift failed to record the attempt at all — no pickup-booking row exists to report on. When this happens, `orderNumber` above may still reflect the carrier's pickup reference via a best-effort background sync, even though `pickupStatus` reads `NOT_BOOKED`.
  serviceCode: string
  serviceDescription?: string | null  // Human-readable service name as returned by the carrier at booking time (e.g. "Road Express")
  source: API | PORTAL  // Whether the shipment was booked via the API or the portal UI
  fromSuburb?: string | null
  fromPostcode?: string | null
  fromState?: string | null  // Upper-cased state/territory code
  toSuburb?: string | null
  toPostcode?: string | null
  toState?: string | null  // Upper-cased state/territory code
  receiverCompany?: string | null
  receiverName?: string | null
  pieceCount?: integer | null  // Total number of pieces booked
  declaredWeightKg?: number | null  // Total declared weight in kilograms, as a JSON number
  declaredCubicM3?: number | null  // Total declared volume in cubic metres, as a JSON number
  quotedPriceExGstMinor?: integer | null  // Ex-GST price recorded at booking, in minor currency units (cents)
  quotedGstMinor?: integer | null  // GST component recorded at booking, in minor units
  quotedPriceIncGstMinor?: integer | null  // Inc-GST price recorded at booking, in minor units
  quotedCurrency?: string | null
  committedEta?: string | null  // Legacy encoding: the carrier's ETA wall clock with a literal `Z`; not an instant. Its date part is the ETA date. Use `committedEtaDate` for the date and `committedEtaAt` for the instant.
  quotedTransitDays?: integer | null  // The quoted transit recorded at booking, in business days, for every carrier.
  collectionDateTime: string  // Pickup ready time as booked, wall-clock local to the collection address (no TZ suffix)
  createdAt: string
  manifested: boolean  // Whether the shipment has been included in a manifest
  manifestedAt?: string | null
  collectedAt?: string | null  // Legacy encoding of the first collection scan: for TNT AU the carrier's local wall clock with a literal `Z` (not an instant), for other carriers the instant. Null until collected. Use `collectedAtInstant`.
  deliveredAt?: string | null  // Legacy encoding of the delivery time: for TNT AU the carrier's local wall clock with a literal `Z` (not an instant), for other carriers the instant. Null until delivered. Use `deliveredAtInstant`.
  dangerousGoods: boolean
  payer: Sender | Receiver | ThirdParty
  payerAccountNumber?: string | null  // The account billed for this shipment, when it wasn't the connector's own.
  bookedWithWarnings: boolean  // The carrier accepted the booking but returned non-fatal warnings (see `bookingWarnings` on the detail endpoint)
  createdByUserId?: string | null  // Portal user who booked the shipment. Null for API bookings.
  quoteId?: string | null  // The rate-shop (`quoteId` from a rates response) this booking was linked to, if any
  rateId?: string | null  // The specific rate (`rateId`) this booking was linked to, if any
  cancelledAt?: string | null  // When this shipment was cancelled in DataShift. Null unless `status` is `CANCELLED`. See `POST /shipments/cancel` — cancellation never notifies the carrier.
  cancelSource?: PORTAL | API  // How the cancellation was initiated. Null unless cancelled.
  cancelReason?: string | null  // The reason given at cancel time, as `"CATEGORY: reason"` when a category was supplied, otherwise the bare reason. Null unless cancelled.
  cancelledByUserId?: string | null  // Portal user who cancelled the shipment. Null for an API-key cancellation or an uncancelled shipment.
  pickupReference: string | null  // The carrier's pickup reference, once a pickup is booked.
  carrierOrderNumber: string | null  // The carrier's own order or consignment reference from the booking.
  committedEtaDate: string | null  // The ETA date committed at booking (`YYYY-MM-DD`, the destination's date, no timezone). Frozen at booking; later carrier ETAs are in `latestCarrierEtaDate`.
  committedEtaByLocalTime: string | null  // Deliver-by time on `committedEtaDate` in the destination's local time (`HH:mm`), when known.
  committedEtaSource: CARRIER_SERVICE | CARRIER_LANE | DERIVED_FROM_TRANSIT | CARRIER_TRACKING | CLIENT  // Where the committed ETA came from: `CARRIER_SERVICE` (the carrier's ETA for this service), `CARRIER_LANE` (one ETA for every service on the lane, BEX AU), `DERIVED_FROM_TRANSIT` (dispatch date plus business days), `CARRIER_TRACKING` or `CLIENT` (sent by the caller).
  committedEtaAt: string | null  // The committed ETA as an instant, or null for a date-only ETA; never take its date part as the ETA date (a 09:00 AEST ETA is 23:00Z the previous day).
  quotedGstSource: CARRIER | DERIVED | CLIENT  // Where `quotedGstMinor` came from: `CARRIER`, `DERIVED` (10%, rounded half-up) or `CLIENT`.
  quotedTransitCalendarDays: integer | null  // Calendar days from dispatch to the committed ETA, for reference; `quotedTransitDays` counts business days.
  collectionWindow: {
    date: string
    readyTime: string
    closeTime: string
    timezone: string
  }  // The booked collection window in the collection address's local time. Named `collectionWindow` because `collection` is the collection address.
  lifecycleStatus: BOOKED | IN_TRANSIT | OUT_FOR_DELIVERY | DELIVERED | EXCEPTION | CANCELLED  // `BOOKED`, `IN_TRANSIT`, `OUT_FOR_DELIVERY`, `DELIVERED`, `EXCEPTION` or `CANCELLED`, derived from the booking and its tracking.
  trackingCoverage: MILESTONES | DELIVERY_ONLY | NONE  // `MILESTONES` (collection and delivery are tracked, TNT AU), `DELIVERY_ONLY` (BEX AU, TGE AU) or `NONE` (not tracked yet).
  collectedAtInstant: string | null  // When the consignment was collected, as an instant.
  deliveredAtInstant: string | null  // When the consignment was delivered, as an instant.
  collectedAtSource: STRUCTURED | MAPPED_CODE | KEYWORD | POD | FIRST_SCAN  // How the milestone was detected: `STRUCTURED`, `MAPPED_CODE`, `KEYWORD`, `POD` or `FIRST_SCAN`.
  deliveredAtSource: STRUCTURED | MAPPED_CODE | KEYWORD | POD | FIRST_SCAN  // How the milestone was detected: `STRUCTURED`, `MAPPED_CODE`, `KEYWORD`, `POD` or `FIRST_SCAN`.
  deliveredLocalDate: string | null  // The delivery date at the destination (`YYYY-MM-DD`). Compare it with `committedEtaDate` for on-time.
  latestCarrierEtaDate: string | null  // The latest ETA date the carrier reported through tracking, if any.
}

ShipmentListResponse

{
  total: integer  // Total number of shipments matching the filters (may exceed the current page)
  limit: integer  // Page size used for this response
  offset: integer  // Offset used for this response
  shipments: ShipmentSummary[]
}

ShipmentDetail

ShipmentSummary & {
  payerSubAccount?: string | null  // TGE SubAccountCode, when one was supplied.
  payerThirdPartyAddress?: BookingAddress  // TNT AU third-party billing address, when one was supplied.
  sender: BookingAddress
  collection: BookingAddress
  receiver: BookingAddress
  items: Items
  collectionCloseTime: string
  specialInstructions?: string | null
  barcodes: string[]  // Carrier-issued item barcodes (BEX AU). Empty for carriers that don't issue them.
  bookingWarnings?: {
    code: string
    message: string
  }[]  // Non-fatal warnings the carrier returned with the booking. Null when there were none.
  labelReturnedAtBooking: boolean  // Whether the carrier returned the label in the booking response itself
  bookingCutoff?: string | null  // The quoted booking cutoff recorded at booking (HH:mm:ss)
  collectionCutoff?: string | null  // The quoted collection cutoff recorded at booking (HH:mm:ss)
  dangerousGoodsDetails?: object  // The dangerous goods declaration as booked (same shape as the booking request's `dangerousGoodsDetails`). Null when `dangerousGoods` is false.
}

ShipmentDetailResponse

{
  shipment: ShipmentDetail
}

CancelEligibilityRequest

{
  ids: string[]  // Datashift shipment IDs to check. Deduplicated and capped at 100.
}

CancelWarning

{
  code: MANIFESTED | PICKUP_BOOKED
  message: string
}

CancelEligibilityResult

{
  id: string
  eligible: boolean
  code?: NOT_FOUND | ALREADY_CANCELLED | DELIVERED | IN_CARRIER_NETWORK  // Present only when `eligible` is false — the first blocking check that matched.
  message?: string  // Present only when `eligible` is false.
  warnings: CancelWarning[]  // Non-blocking, stack independently. Only ever present when `eligible` is true.
}

CancelEligibilityResponse

{
  results: CancelEligibilityResult[]  // Same (deduped) order as the request ids.
  carrierNotified: false  // Always false — this endpoint never contacts a carrier.
}

CancelShipmentsRequest

{
  ids: string[]
  reason: string  // Required free-text reason, persisted as `cancelReason` on the shipment.
  category?: DUPLICATE | BOOKED_IN_ERROR | ORDER_CANCELLED | REBOOKED | OTHER  // Optional. When given, persisted as `"CATEGORY: reason"`.
  acknowledgeCarrierNotNotified: true  // Required, must be exactly `true`. Confirms you understand the carrier is not notified and, if a label was already printed, it must be destroyed.
  acknowledgeManifested?: boolean  // Blanket acknowledgement: `true` allows every manifested shipment in the request to be cancelled. A manifested shipment not covered by this or `acknowledgeManifestedIds` is skipped with `MANIFEST_ACK_REQUIRED`.
  acknowledgeManifestedIds?: string[]  // Per-shipment acknowledgement: manifested shipments that may be cancelled. Preferred over `acknowledgeManifested` — list the ids your eligibility check showed as `MANIFESTED`, so a shipment manifested after that check is skipped instead of cancelled unseen.
}

CancelSkipped

{
  id: string
  code: NOT_FOUND | ALREADY_CANCELLED | DELIVERED | IN_CARRIER_NETWORK | MANIFEST_ACK_REQUIRED
  message: string
}

CancelShipmentsResponse

{
  cancelled: string[]  // Ids that were actually cancelled.
  skipped: CancelSkipped[]  // Ids that were not cancelled, with why.
  carrierNotified: false  // Always false — no carrier is ever notified by this endpoint.
  warning: string  // Always present — the DataShift-only cancellation caveat as display text. Surface it to whoever triggered the cancel.
  manifestedWarning?: string  // Present only when at least one cancelled shipment had already been manifested to the carrier.
}

TrackingEvent

{
  timestamp: string  // Scan time as the carrier sent it. TNT AU sends no offset (the scan location's wall clock); BEX AU and TGE AU send their own format, with or without an offset. Deprecated; use `at`.
  status: string
  location?: string | null
  description?: string | null
  at: string | null  // The scan time as an instant; null when the carrier's timestamp is unreadable.
  code: BOOKED | PICKED_UP | IN_TRANSIT | AT_DEPOT | OUT_FOR_DELIVERY | DELIVERY_ATTEMPTED | DELIVERED | EXCEPTION | RETURNED | INFO  // The canonical event code: `BOOKED`, `PICKED_UP`, `IN_TRANSIT`, `AT_DEPOT`, `OUT_FOR_DELIVERY`, `DELIVERY_ATTEMPTED`, `DELIVERED`, `EXCEPTION`, `RETURNED` or `INFO`.
}

TrackingPod

{
  deliveryNotation?: string | null
  itemsReceived?: string | null
  signedBy?: string | null
  signatureImageUrl?: string | null  // Direct link to the signature image, always https
}

TrackingInfo

{
  currentStatus: string
  eta?: string | null  // ETA as the carrier sent it: `YYYY-MM-DD`, an ISO date-time, or `DD/MM/YYYY` (BEX AU). Deprecated; use `etaDate`.
  collectedAt?: string | null  // Collection time as the carrier sent it (TNT AU: no offset, the carrier's local wall clock). Present only when collected. Deprecated; use `collectedAtInstant`.
  deliveredAt?: string | null  // Delivery time as the carrier sent it (TNT AU: no offset, the carrier's local wall clock). Present only when delivered. Deprecated; use `deliveredAtInstant`.
  detectedBy?: structured | mapped-code | keyword | pod  // How the carrier signal behind `collectedAt` / `deliveredAt` was detected. Present only when one was. Deprecated; use `collectedAtSource` / `deliveredAtSource`.
  events: TrackingEvent[]  // Scan events in reverse-chronological order (most recent first) — guaranteed for every carrier.
  pod?: TrackingPod  // Proof of delivery, when the carrier supports it and it is available for this shipment. Not marked `nullable` deliberately: when unavailable this key is omitted from the response entirely (not present in the JSON) rather than sent as `null` — codegen that treats `nullable: false` + absent-key as "always present" will be wrong here, so check for the key's presence, not its value.
  etaDate: string | null  // The ETA as a date (`YYYY-MM-DD`), whatever format the carrier sent `eta` in.
  collectedAtInstant: string | null  // When the consignment was collected, as an instant. Offset-less carrier times are read in the collection state's zone.
  deliveredAtInstant: string | null  // When the consignment was delivered, as an instant. Offset-less carrier times are read in the destination state's zone.
  lifecycleStatus: BOOKED | IN_TRANSIT | OUT_FOR_DELIVERY | DELIVERED | EXCEPTION | CANCELLED  // The status this tracking response implies: `BOOKED`, `IN_TRANSIT`, `OUT_FOR_DELIVERY`, `DELIVERED`, `EXCEPTION` or `CANCELLED`.
  collectedAtSource: STRUCTURED | MAPPED_CODE | KEYWORD | POD | FIRST_SCAN  // How the milestone was detected: `STRUCTURED`, `MAPPED_CODE`, `KEYWORD`, `POD` or `FIRST_SCAN`.
  deliveredAtSource: STRUCTURED | MAPPED_CODE | KEYWORD | POD | FIRST_SCAN  // How the milestone was detected: `STRUCTURED`, `MAPPED_CODE`, `KEYWORD`, `POD` or `FIRST_SCAN`.
}

TrackingResponse

{
  supported: boolean  // `false` when the matched carrier does not expose a tracking API
  success?: boolean  // Always `true` on a 200 response. Kept for compatibility.
  tracking?: TrackingInfo  // Null if tracking is not yet available or not supported
}

PrintDevice

{
  id: string  // Use as `deviceId` in print requests
  label: string  // Human-readable name for the device (set during registration)
  printerName?: string | null  // Deprecated — the device's preferred printer name, kept for backward compatibility. Prefer `printers[]`.
  lastUsedAt?: string | null  // When this device last received a print job
  createdAt: string
  printers: PrintDevicePrinter[]  // Every printer this device has reported. Empty for a device running a pre-multi-printer label-client version.
}

PrintDevicePrinter

{
  id: string  // Use as `printerId` in print requests to override the device's preferred printer
  name: string  // OS printer name as reported by the label client
  isPreferred: boolean  // Whether this is the printer the device currently defaults to when no `printerId` override is given
  isOnline?: boolean | null  // Best-effort online status as of the last device sync — may be stale
  lastSeenAt?: string  // When the device last reported this printer
}

PrintDeviceListResponse

{
  devices: PrintDevice[]
}

PrintRequest

{
  deviceId: string  // ID of the print device to send the label to (from `GET /print-devices`)
  printerId?: string  // Optional id of a specific printer on the target device; defaults to the device's preferred printer when omitted.
}

PrintJobCreated

{
  jobId: string  // Datashift print job ID
}

LabelNotAvailableError

{
  message: string
  code: LABEL_NOT_AVAILABLE
}

PrintNotFoundError

{
  message: string
  code: LABEL_NOT_AVAILABLE | DEVICE_NOT_FOUND | SHIPMENT_NOT_FOUND
}

CarrierError

{
  message: string
  code?: string
}

Error

{
  message: string
  errors?: {
    message: string
    field?: string  // Field path for validation errors (e.g. `sender.postcode`)
    code?: string  // Carrier error code for carrier-returned errors
  }[]  // Validation errors use `{ field, message }`; carrier errors use `{ code, message }`
}