API Reference
Track ocean containers, bills of lading, and bookings across major carriers through one REST API. Submit a reference, and receive normalized milestone events by polling or by webhook.
Getting access
API access is available with a subscription plan. A 7-day free trial is available. Pay-as-you-go credits include the full web app but not the API. Once subscribed, issue a key from the API keys page in the app. You never lose access to shipments you’re already tracking.
Authentication
Every request requires a bearer token in the Authorization header. Keys are issued per organization and are scoped to it, so that a key can only ever read and write your own shipments.
curl https://api.matilogistics.com/v2/tracking_requests \ -H "Authorization: Bearer tnt_your_key_here"
Core concepts
Understanding the primary resources and their lifecycle is essential for integrating with the API.
| Object | What it is |
|---|---|
tracking_request | The initial request to track a reference number. Created immediately with a status of pending, then resolved asynchronously against the carrier. |
shipment | The verified carrier tracking data (e.g., vessel, voyage, ETA, milestone events). Attached as the tracked_object on the tracking_request once resolved. |
container | An individual container unit associated with a shipment. A single bill of lading (shipment) may encompass multiple containers. |
Asynchronous resolution workflow
Carrier lookups are processed asynchronously and typically complete within seconds to a few minutes, depending on carrier response times.
New references. Creating a tracking request for a new reference returns 201 Created with status: "pending" and no tracked_object payload.
Existing references. If the reference is already tracked, the endpoint returns status: "succeeded" immediately, populated with the tracked_object.
Integration guidance. Evaluate the response status synchronously. If status is succeeded, process the tracked_object payload immediately. Otherwise, rely on the tracking_request.succeeded webhook event rather than polling the endpoint.
Quickstart
1. Submit a reference
curl -X POST https://api.matilogistics.com/v2/tracking_requests \
-H "Authorization: Bearer tnt_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"request_type": "container",
"request_number": "MRKU3643730"
}'request_type defaults to auto and may be omitted. Under auto, the reference is classified server-side: container by ISO 6346 shape and check digit, bill of lading when letter-prefixed, booking for bare digits. A reference whose shape is invalid, such as one with an incorrect check digit, is rejected with 422 naming the reason before any carrier is queried. An explicit container, bol or booking value is treated as authoritative and passed through unchanged.
Supply carrier where known, as either a SCAC ( "MAEU") or a carrier name ("Maersk"), to make routing deterministic. When omitted, the carrier is detected from the reference; see Finding the carrier for the detection sequence and the response when detection is inconclusive. Set continue_tracking: false to perform a one-off lookup rather than establish a monitored shipment.
201 Created
{
"id": "e7da5c9f-d56d-44ae-b820-1434d48120d6",
"request_type": "container",
"request_number": "MRKU3643730",
"scac": "MAEU",
"status": "pending",
"error_code": null,
"error_message": null,
"shipment_id": null,
"created_at": "2026-07-29T16:42:37.316057+00:00"
}2. Read the result
Once resolved, status becomes succeeded and the shipment is embedded as tracked_object, trimmed here for length:
200 OK
{
"id": "e7da5c9f-d56d-44ae-b820-1434d48120d6",
"request_type": "container",
"request_number": "MRKU3643730",
"scac": "MAEU",
"status": "succeeded",
"shipment_id": "c5872f9d-03fb-4aa8-9b56-c513d9d4b6c4",
"tracked_object": {
"id": "c5872f9d-03fb-4aa8-9b56-c513d9d4b6c4",
"carrier": "maersk",
"tags": ["VESSEL_DEPARTED", "VESSEL_ARRIVED"],
"original_eta": "2026-06-05T14:00:00Z",
"original_etd": "2026-05-20T08:00:00Z",
"summary": {
"vessel": "EVER GIVEN",
"voyage": "123E",
"eta": "2026-06-05T14:00:00Z",
"last_free_day": null,
"location": "Los Angeles, CA"
},
"tracking_enabled": true,
"stale": false,
"containers": [
{
"id": "088a50e4-3841-4d2f-a21c-06f30d6c3103",
"container_no": "MRKU3643730",
"last_free_day": null,
"availability": null,
"hold_flag": false,
"last_event_at": "2026-06-05T14:00:00"
}
],
"events": [
{
"containerNo": "MRKU3643730",
"carrier": "maersk",
"sourceLabel": "Maersk",
"milestoneCode": "VESSEL_DEPARTURE",
"milestoneDescription": "Vessel departed",
"location": "Shanghai, CN",
"eventTime": "2026-05-20T08:00:00Z",
"demurrageFlag": false,
"holdFlag": false
},
{
"containerNo": "MRKU3643730",
"carrier": "maersk",
"sourceLabel": "Maersk",
"milestoneCode": "VESSEL_ARRIVAL",
"milestoneDescription": "Vessel arrived",
"location": "Los Angeles, CA",
"eventTime": "2026-06-05T14:00:00Z",
"eta": "2026-06-05T14:00:00Z",
"demurrageFlag": false,
"holdFlag": false
}
]
}
}milestoneCode is normalized across all carriers: a given code carries the same meaning regardless of source. milestoneDescription retains the carrier’s original text. Wording that cannot be mapped is preserved rather than discarded. See Milestones for the complete vocabulary.
Route
A shipment also carries route: the locations named by the carrier, in the order stated. This includes intermediate ports a container passes through, which are not otherwise apparent from the origin and destination alone. route reflects the carrier’s own statement; see Transshipments for the derived view, including dwell time at each hub.
"route": [
{
"sequence": 0,
"role": "POL",
"location": "Chittagong",
"location_code": null,
"country": null,
"terminal": null,
"arrival_time": null,
"arrival_is_actual": null,
"departure_time": "2026-05-14T07:15:42Z",
"departure_is_actual": true,
"vessel": null,
"voyage": null,
"mode": null
},
{
"sequence": 1,
"role": "POD",
"location": "Xiaochan Beach",
"arrival_time": "2026-06-02T11:20:00Z",
"arrival_is_actual": true,
"vessel": "EM SPETSES"
},
{
"sequence": 2,
"role": "POD",
"location": "New York",
"arrival_time": "2026-07-20T05:37:28Z",
"arrival_is_actual": false,
"vessel": "OOCL SUNFLOWER"
}
]role is a free-text label, not an enumeration. It contains the carrier’s own designation for the location, verbatim. Values observed across carriers include "POL", "pol", "receipt", "Place of Receipt" and "route-location.transit.sea". These are not normalized: consolidating them into a single vocabulary would discard distinctions the carriers themselves draw, as a place of receipt is not equivalent to a port of loading. Clients should treat the value as opaque.arrival_is_actual and departure_is_actual reflect the carrier’s own actual-versus-estimated designation. A value of null indicates the carrier did not specify, which is distinct from an estimate and should not be rendered as one. Fields the carrier does not provide are returned as null, and route is an empty array for carriers that publish no routing data. Route data is never synthesized.
Reading times correctly
Most carriers publish a local wall-clock time, such as 16-Jul-2026 06:00, with no UTC offset stated. Such a value cannot be converted to an absolute instant without inventing information, so it is returned exactly as the carrier published it, accompanied by a flag identifying it as local.
| Field | Where | Meaning |
|---|---|---|
eventTimeIsLocal | event | true → eventTime is the carrier’s own wall clock at that place. The trailing Z is a storage convention, not a claim about UTC. |
eventTimeZone | event | The carrier’s own zone label, verbatim ( "PDT", "ICT"). Usually null, as most carriers do not state one. |
times_are_local | summary | Applies to eta, etd, ata, atd and last_free_day. |
times_are_local | route point | Applies to that point’s arrival_time and departure_time. |
Do not convert local times
Interpreting 2026-07-16T06:00:00Z as UTC and rendering it in US Pacific time produces “Jul 15, 11:00 PM”, which is incorrect by both hour and calendar day and contradicts the carrier’s own published time. Render the value as supplied, in most libraries by formatting in UTC, and label it with eventTimeZone where present.
The same applies to date-only values such as last_free_day. "2026-07-20" parses as UTC midnight, so any conversion to a western time zone yields the preceding day. An incorrect last free day carries direct demurrage exposure.
The summary object carries no zone label. Its arrival values pertain to the discharge port and its departure values to the loading port, so a single shipment-level label would be incorrect for one or the other. Zone labels are provided on events and route points, each of which corresponds to a single location.
Bills covering several containers
A bill of lading frequently covers multiple containers, and those containers do not move as a unit: they may discharge on different dates, gate out to different hauliers, and be returned empty weeks apart. Tracking is therefore performed per container. Every event carries containerNo, and the shipment enumerates each unit in containers.
"containers": [
{ "container_no": "TGBU4019273", "last_free_day": "2026-07-24", "hold_flag": false },
{ "container_no": "TXGU5832207", "last_free_day": "2026-07-24", "hold_flag": false }
],
"events": [
{ "containerNo": "TGBU4019273", "milestoneCode": "GATE_OUT_DELIVERY", "eventTime": "..." },
{ "containerNo": "TXGU5832207", "milestoneCode": "GATE_OUT_DELIVERY", "eventTime": "..." }
]Include containerNo in any de-duplication key
Two containers discharged within the same hour at the same terminal produce events that are identical except for the container number. A de-duplication key omitting containerNo will collapse them, causing the shipment to report fewer milestones than the carrier published.
Events with containerNo: null are bill-level: stated once for the shipment as a whole and not attributable to an individual container.
Milestones
Events carry a milestoneDescription (the carrier’s exact text) and a milestoneCode (the normalized value). For unmapped events the code is either empty or contains the carrier’s own text. Match against the vocabulary below rather than testing for an empty value.
Ocean moves: EMPTY_PICKUP, GATE_IN, LOAD, VESSEL_DEPARTURE, TRANSSHIPMENT_DISCHARGE, TRANSSHIPMENT_LOAD, VESSEL_ARRIVAL, VESSEL_BERTHING, DISCHARGE, AVAILABLE_FOR_PICKUP, GATE_OUT_DELIVERY, EMPTY_RETURN.
Inland: RAIL_LOAD, RAIL_DEPARTURE, RAIL_ARRIVAL, RAIL_UNLOAD, INLAND_TRANSIT, INLAND_RECEIVED.
Schedule: ETA_UPDATE, ETD_UPDATE, LFD_UPDATE.
Document, customs and release states: CUSTOMS_HOLD, CUSTOMS_RELEASE, CUSTOMS_FILED, HOLD_PLACED, HOLD_RELEASED, CARRIER_RELEASE, ISF_FILED, BOOKING_SUBMITTED, BOOKING_CONFIRMED, DOCS_SUBMITTED, BL_ISSUED, BL_SURRENDERED, IN_BOND_AUTHORIZED, NOTIFICATION_SENT, CFS_STRIPPED, CFS_STUFFED, SERVICES_COMPLETED.
tags on the shipment is the progress summary: the set of milestones reached to date, in journey order. EMPTY_PICKED_UP, GATE_IN, LOADED, VESSEL_DEPARTED, TRANSSHIPPED, VESSEL_ARRIVED, VESSEL_BERTHED, DISCHARGED, AVAILABLE_FOR_PICKUP, RAIL_DEPARTED, RAIL_ARRIVED, INLAND_IN_TRANSIT, INLAND_RECEIVED, DELIVERED, EMPTY_RETURNED. Document states produce no tag, as they are not journey progress.
Last free day
The last free day could appear in three places:
| Location | What it is |
|---|---|
containers[].last_free_day | The last free day for each container. Containers on the same bill of lading may carry different last free days. |
summary.last_free_day | Shipment-level value, applicable to single-container shipments. |
events[].lastFreeDay | The value as recorded at that event, providing an audit trail when a carrier revises it. |
An event coded LFD_UPDATE indicates the carrier has published or revised the value. null indicates no value has been published; it does not indicate unlimited free time. Where a carrier states the value as a calendar date only, refer to Reading times correctly before formatting it.
Transshipments
Transshipments are listed in journey order in summary.transshipments. A direct service has an empty array.
"transshipments": [
{
"port": "TANGER MED",
"arrived_from": "VALENCIA",
"departing_to": "NORFOLK, VA",
"inbound_vessel": "RDO CONCORD",
"outbound_vessel": "SEOUL EXPRESS",
"eta": null,
"ata": "2026-06-25T07:00:00.000Z",
"etd": null,
"atd": "2026-07-04T08:06:00.000Z",
"atd_is_load": true,
"dwell_hours": 217.1,
"evidence": ["Vessel arrived - TANGER MED", "Loaded - TANGER MED - SEOUL EXPRESS 625W"]
}
]dwell_hours is the elapsed time at the intermediate port. It is computed from actual values only: where either side is an estimate, or the carrier published no departure, the value is null.
Inferred departures
atd_is_load: true indicates the departure value is derived from the onward load rather than a stated departure. Several carriers publish an actual arrival at the intermediate port but no actual departure, in which case the load onto the onward vessel is the only available signal.
These entries are derived from the carrier’s events rather than stated directly, so evidence carries the verbatim wording underlying each entry. An intermediate port that appears only in a carrier’s forward schedule, with no actual movements recorded, is represented in route rather than here.
Finding the carrier
Supplying the carrier is recommended wherever it is known. A bill of lading generally carries the issuing carrier’s SCAC as a prefix, but booking numbers rarely identify the carrier, and a container number identifies only the equipment owner, which for leased units is a leasing company rather than the operating line.
Where carrier is not supplied, detection proceeds in sequence: the SCAC prefix on a bill of lading or booking; the ISO 6346 owner prefix on a container; and, where that prefix belongs to a leasing company (Triton, SEACO, CAI, Textainer, Touax, Blue Sky, SeaCube), the lessor’s public lookup to identify the on-hire operator. If no stage resolves a carrier, the request completes with status: "failed" and error_code: "carrier_unknown". Resubmitting the same reference with carrier specified updates the existing request, retaining its id, rather than creating a duplicate.
To determine the carrier before submission, or to populate a carrier selector, query the detection endpoint directly:
curl "https://api.matilogistics.com/v2/carriers/detect?reference=TCLU1234568" \
-H "Authorization: Bearer tnt_your_key_here"
{
"normalized": "TCLU1234568",
"valid": true,
"ref_type": "container",
"carrier": null,
"scac": null,
"carrier_name": null,
"method": null,
"lessor": "triton",
"lessor_name": "Triton",
"on_hire": null,
"message": "Leased container owned by Triton. Pass deep=true to ask the lessor who has it on hire (may take several seconds)."
}The default call performs reference validation against the prefix maps. method identifies which stage resolved the carrier ( container_prefix, scac_prefix or lessor). Add deep=true to perform the lessor lookup for a leased container. This takes several seconds, counts against the write rate limit, and returns on_hire: true together with the operating carrier, or false where the container is idle in a depot and no active shipment exists. Florens does not disclose the operating carrier through its lookup, so the carrier must be supplied explicitly for Florens-owned equipment.
Endpoints
Tracking requests
| Submit a reference for tracking. | |
| List your tracking requests. Cursor-paginated. | |
| One request, with its resolved shipment. | |
Retrieve multiple records by id in a single call: {"ids": [...]}. | |
| Stop tracking. Other subscribers tracking the same container are unaffected. |
Shipments & containers
| List shipments. | |
| One shipment with its full event history. | |
| Containers on a shipment. | |
| One container. | |
| Milestone events for one container. |
Reference data
| Carrier coverage and SCACs; the source of the table below. | |
| Detect the carrier for a reference before submission. See Finding the carrier. | |
| Terminals available for direct query. |
Pagination
List endpoints are cursor-paginated. Pass the previous response’s meta.next_cursor as ?cursor=; stop when has_more is false.
{
"data": [ ],
"meta": { "next_cursor": null, "has_more": false }
}Webhooks
Register an endpoint to receive status changes by POST rather than polling. Each endpoint is issued its own signing secret, returned once at creation.
curl -X POST https://api.matilogistics.com/v2/webhooks \
-H "Authorization: Bearer tnt_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/mati"}'
201 Created
{
"id": "86eb5bb5-87bb-4263-a455-624f42916dd2",
"url": "https://example.com/hooks/mati",
"secret": "78572cbf8d27171c9a2c931083053ec5afc138fb7224c604",
"active": true,
"event_types": null
}event_types: null subscribes to everything. Pass an array to narrow it. Supply a secret at creation where the receiving system is already configured with one, or rotate it subsequently via POST /v2/webhooks/{id}/rotate_secret.
Event types
tracking_request.succeeded | A reference resolved; the shipment is available. |
tracking_request.failed | The carrier lookup failed. |
tracking_request.not_found | The carrier has no record of this reference. |
shipment.updated | New milestone data on a tracked shipment. |
container.transport_event | A container-level movement. |
Verifying a delivery
Every delivery carries X-TNT-Event and X-TNT-Signature, an HMAC-SHA256 of the raw request body, hex-encoded, keyed with your endpoint’s secret. Compute the signature over the raw bytes as received, before JSON parsing or re-serialization; otherwise it will not match.
import hmac, hashlib
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)2xx promptly and process asynchronously; a slow handler is treated as a failure and retried. Handlers must be idempotent: a repeated event should be a no-op rather than a second shipment update.Errors
Errors share a common envelope. request_id is present on every response, successful or otherwise. Include it when reporting an issue to identify the specific call.
401 Unauthorized
{
"error": {
"code": "unauthenticated",
"message": "Missing bearer token",
"request_id": "b40ca7a0c8b14aec8b7eafd7598b07b3"
},
"detail": "Missing bearer token"
}Validation failures include a fields array identifying the invalid input:
422 Unprocessable
{
"error": {
"code": "unprocessable",
"message": "Request body or parameters failed validation.",
"request_id": "d662fa2499584bf091748a050d49b7c7",
"fields": [
{
"type": "literal_error",
"loc": ["body", "request_type"],
"msg": "Input should be 'container', 'bol' or 'booking'",
"input": "nope"
}
]
}
}| Status | Meaning |
|---|---|
| 400 | Malformed request, or an unknown carrier hint. |
| 401 | Missing, invalid, or revoked API key. |
| 403 | The key is not permitted to act as the subscriber it named. |
| 404 | No such object, or one outside the requesting organization. |
| 422 | Well-formed but unprocessable: a validation failure, or a recognized carrier not yet supported. |
| 429 | Rate limit exceeded. |
422 on create states the reason a carrier is unavailable, distinguishing an unsupported carrier from an invalid reference: "Carrier ‘Crowley’ is reachable only behind a customer login and can’t be tracked publicly."Rate limits
Limits are applied per organization on a fixed hourly window. Write operations are the constrained resource, as each may trigger a live carrier lookup; read operations are served from stored data.
| Class | Limit | Covers |
|---|---|---|
| Writes | 1,000 / hour | Creating tracking requests, webhook changes |
| Reads | 10,000 / hour | Every GET |
Exceeding a limit returns 429. These are default values and can be raised on request.
Carrier coverage
Coverage spans ocean carriers and NVOCCs. The list of the current coverage is listed below. You can also query this complete list dynamically using the GET /v2/carriers API endpoint. New coverage requests can be made in the app.
| Carrier | SCAC | Type |
|---|---|---|
| Atlantic Container Line | ACLU | Ocean carrier |
| CMA CGM | CMDU | Ocean carrier |
| COSCO | COSU | Ocean carrier |
| Evergreen | EGLV | Ocean carrier |
| Expeditors International | EXDO | NVOCC |
| Hapag Lloyd | HLCU | Ocean carrier |
| Hecny Shipping | HYSL | NVOCC |
| HEDE Shipping | HDUJ | Ocean carrier |
| HMM | HDMU | Ocean carrier |
| Honour Lane Logistics | HNLT | NVOCC |
| Kuehne + Nagel | BANQ | NVOCC |
| Maersk | MAEU | Ocean carrier |
| Matson | MATS | Ocean carrier |
| MSC | MSCU | Ocean carrier |
| ONE | ONEY | Ocean carrier |
| OOCL | OOLU | Ocean carrier |
| Orient Express Container | OERT | NVOCC |
| Pacific International | PCIU | Ocean carrier |
| Shipco Transport | SHPT | NVOCC |
| SM Line | SMLM | Ocean carrier |
| Turkon | TRKU | Ocean carrier |
| Wan Hai Lines | WHLC | Ocean carrier |
| Yang Ming | YMLU | Ocean carrier |
| Zim | ZIMU | Ocean carrier |