ShipScanner for developers
Container tracking over HTTPS. Send a container number, get back where it is, where it has been and when it is expected — as JSON, authenticated with a bearer token.
Connect SGL Advisor
SGL Advisor reads container tracking from ShipScanner. Tracking is the only thing the
connection does for now, so its token needs the tracking scope
and nothing else.
- 1
Create a token for SGL Advisor
In ShipScanner, open Account → API tokens. Name it “SGL Advisor”, tick the tracking scope only, and create it. The token is shown once — copy it then.
- 2
Give SGL Advisor the base URL and the token
Store the token as a server-side secret in SGL Advisor, never in browser code. Every request sends it in the Authorization header.
- 3
Track a container
Call the endpoint below with a container number. A 200 with status "ok" means the connection works end to end.
- Base URL
- https://shipscanner.co/api/v1
- Header
- Authorization: Bearer ssk_…
- Token scope
- tracking
Open API tokens — revoke the token there at any time and the connection stops on its next request.
Authentication
Every request carries an API token in the Authorization header.
Tokens start with ssk_, belong to the account that
created them and are shown once, at creation. A request counts against that account’s plan.
curl https://shipscanner.co/api/v1/tracking/containers/MSKU1234565 \
-H "Authorization: Bearer ssk_your_token_here" Keep tokens on a server. Anything shipped to a browser or a mobile app can be read by its user.
Track a container
/api/v1/tracking/containers/{number} number is an ISO 6346 container number such as
MSKU1234565: four letters, six digits and a check digit.
The carrier is detected from the number; there is nothing else to send.
Response
The shape is exact; the values below are illustrative, not a real shipment.
{
"status": "ok",
"data": {
"containerNumber": "MSKU1234565",
"status": "IN_TRANSIT",
"isoType": "45G1",
"carrierScac": "MAEU",
"vessel": {
"name": "EXAMPLE VESSEL",
"imo": "9000001",
"voyage": "123E"
},
"origin": {
"locode": "TRIST",
"name": "Istanbul",
"country": "TR",
"lat": 41.01,
"lng": 28.98
},
"destination": {
"locode": "NLRTM",
"name": "Rotterdam",
"country": "NL",
"lat": 51.95,
"lng": 4.14
},
"departedAt": "2026-01-04 18:30:00",
"arrivedAt": null,
"eta": "2026-01-16 06:00:00",
"events": [
{
"at": "2026-01-04 18:30:00",
"atUtc": null,
"description": "Vessel departure",
"code": "VD",
"actual": true,
"location": {
"locode": "TRIST",
"name": "Istanbul",
"country": "TR",
"lat": 41.01,
"lng": 28.98
},
"vessel": {
"name": "EXAMPLE VESSEL",
"imo": "9000001",
"voyage": "123E"
}
}
]
},
"meta": {
"requestId": "0b0f6a1e-2f0c-4a53-9d0e-5c1f3f1f7a10",
"mode": "live"
}
} | Field | Type | Meaning |
|---|---|---|
| containerNumber | string | The number as tracked: upper case, no spaces. |
| status | string | null | The carrier’s current status for the container. |
| isoType | string | null | ISO 6346 size and type code, for example 45G1. |
| carrierScac | string | null | SCAC of the operating carrier, when it was identified. |
| vessel | object | null | name, imo and voyage of the vessel carrying it. |
| origin, destination | object | locode (UN/LOCODE), name, country (ISO alpha-2), lat, lng. Any of them may be null. |
| departedAt | string | null | Actual departure from the port of loading. null until it has sailed. |
| arrivedAt | string | null | Actual arrival at the port of discharge. null until it has arrived. |
| eta | string | null | Estimated arrival at the port of discharge. |
| events[] | array | Every reported milestone, oldest first. |
| events[].at | string | null | Local time at the event’s location (the port’s wall clock). |
| events[].atUtc | string | null | The same instant in UTC, when it is known. |
| events[].actual | boolean | null | true has happened; false is planned or estimated. |
| events[].description, code | string | null | The carrier’s wording and event code. |
| events[].location, vessel | object | Same shapes as origin and vessel above. |
A value nobody has reported is null, never an empty string. New fields
may be added to v1; existing ones are not renamed, removed or retyped.
meta.asOf, when present, is the time (UTC) the carrier data in this answer
was read. It appears only when the answer comes from a container ShipScanner is already watching for
changes; an answer without it was read from the carrier feed for this request.
Errors
A failure has the same envelope whatever went wrong. Branch on code;
message is for people and may be reworded.
{
"status": "error",
"code": "not_found",
"message": "No tracking information was found for this container yet."
} | HTTP | code | When |
|---|---|---|
| 400 | invalid_request | Not a valid ISO 6346 container number (4 letters, 7 digits, check digit). |
| 401 | unauthenticated | No token, or the token is unknown, revoked or expired. |
| 402 | quota_exceeded | The account’s plan has no tracking allowance left this month. details carries plan and usage. |
| 403 | insufficient_scope | The token carries neither the tracking nor the read scope. |
| 404 | not_found | No carrier has published tracking for this container yet. |
| 429 | rate_limited | More than 30 lookups in a minute. Wait for Retry-After seconds. |
| 502 | upstream_error | The tracking provider failed. Safe to retry. |
Limits
- 30 requests a minute per token. Each response reports the
budget in
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset; a 429 addsRetry-After. - A monthly tracking allowance set by the account’s plan. Each container found uses one lookup; a 4xx answer, including a container that was not found, uses none.
- Poll gently. Carriers update a container a few times a day at most, so asking about the same one more than hourly spends allowance without learning anything.
A client in a dozen lines
TypeScript, no dependencies. The same four branches cover every answer the endpoint gives.
const SHIPSCANNER_API = 'https://shipscanner.co/api/v1'
export async function trackContainer(number: string) {
const res = await fetch(
`${SHIPSCANNER_API}/tracking/containers/${encodeURIComponent(number)}`,
{ headers: { Authorization: `Bearer ${process.env.SHIPSCANNER_API_TOKEN}` } },
)
const body = await res.json()
if (body.status === 'ok') return body.data
// Branch on `code`, never on `message`.
if (body.code === 'not_found') return null
if (body.code === 'rate_limited') {
throw new Error(`Retry in ${res.headers.get('Retry-After')}s`)
}
throw new Error(`ShipScanner ${body.code}: ${body.message}`)
} Building something this page does not cover? Tell us what you need.