Skip to main content

Vessels API (AIS)

Query the AIS vessel directory and live vessel detail. Both endpoints are read-only and require the ais:read permission.

Scope

This is a query-only API over two static/near-live sources — there is no position-history / track endpoint. Live positions are never persisted to Postgres; they stream over the WebSocket "vessels" channel instead (see WebSocket API) and are cached in Redis for the detail read below. A third, separate endpoint (below) exposes the static-data change-log per MMSI — name/callsign/ship-type/length/beam changes, not positions.

Data Sources​

EndpointSourcePosition fields (lat/lng/heading/course/speedKmh)navStatusfirstSeen
Directory (GET /v1/vessels / vessels)Postgres "Vessel" registry (static, durable catalog of every vessel ever seen)always nullalways nullfrom the registry row
Detail (GET /v1/vessels/{mmsi} / vessel(mmsi))Redis ais:vessel:latest:{mmsi} first (full live snapshot written by the AIS ingest pipeline); falls back to the registry (position fields null) on a cache misspresent on a Redis hit, null on registry fallbackpresent on a Redis hit, null on registry fallbackmerged in from a registry lookup by mmsi regardless of Redis hit/miss; null only if the vessel has no registry row

The registry is a static catalog only (name, callsign, ship type, length, beam, source, first/last-seen) — it has no position column. Live positions live exclusively in the Redis hot cache and the "vessels" WebSocket broadcast; the Redis key is cleared (not just expired) by the AIS writer task once a vessel stops reporting, at which point detail reads fall back to the registry. firstSeen lives ONLY in the registry — Redis has no such field — so a detail read always does a lightweight registry lookup by mmsi to populate it, even on a Redis hit.

Canonical Vessel Shape​

REST (VesselView, snake_case) and GraphQL (GqlVessel, camelCase) expose the same fields:

REST field (snake_case)GraphQL field (camelCase)TypeDescription
ididUUID | nullRegistry-only stable identifier, same presence rule as first_seen/firstSeen (merged in from the registry on a detail Redis hit); null only if the vessel was never registered
mmsimmsiintegerMaritime Mobile Service Identity
namenamestringMay be empty when the source never reported a name
callsigncallsignstring | null
ship_typeshipTypeinteger | nullNumeric ITU ship-type code
nav_statusnavStatusinteger | nullNumeric nav-status code; null on registry fallback
lengthlengthinteger | nullMeters
beambeaminteger | nullMeters
latlatnumber | nullnull on registry fallback
lnglngnumber | nullnull on registry fallback
headingheadinginteger | nullDegrees; null on registry fallback
coursecoursenumber | nullDegrees; null on registry fallback
speed_kmhspeedKmhnumber | nullnull on registry fallback
sourcesourcestringData source identifier
last_seenlastSeentimestampISO 8601
first_seenfirstSeentimestamp | nullISO 8601. Registry-only — populated whenever the vessel has a row in the "Vessel" registry, including on a detail Redis hit (merged in from a registry lookup by mmsi); null only if the vessel was never registered

REST API​

List Vessels​

Paginated vessel directory, sourced from the static registry.

GET /v1/vessels
Authorization: Bearer YOUR_TOKEN

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number (1-indexed)
limitinteger20Items per page (clamped 1–100)
namestring-Case-insensitive substring match on vessel name
ship_typeinteger-Exact numeric ITU ship-type code

Response:

{
"data": [
{
"id": "32b2226c-18f3-452e-9d1f-fab5128110df",
"mmsi": 211234560,
"name": "MS RENA",
"callsign": "DABC",
"ship_type": 70,
"nav_status": null,
"length": 100,
"beam": 20,
"lat": null,
"lng": null,
"heading": null,
"course": null,
"speed_kmh": null,
"source": "local",
"last_seen": "2026-07-28T10:00:00Z",
"first_seen": "2026-01-15T08:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"total_pages": 1
},
"_links": {
"self": "https://api.example.com/v1/vessels?page=1&limit=20"
}
}

Get Vessel​

Vessel detail: Redis hot-read first (full live data), falls back to the static registry on a cache miss.

GET /v1/vessels/{mmsi}
Authorization: Bearer YOUR_TOKEN

Path Parameters:

ParameterTypeDescription
mmsiintegerVessel MMSI

Response (Redis hit — live position present):

{
"data": {
"id": "32b2226c-18f3-452e-9d1f-fab5128110df",
"mmsi": 211234560,
"name": "MS RENA",
"callsign": "DABC",
"ship_type": 70,
"nav_status": 0,
"length": 100,
"beam": 20,
"lat": 49.4,
"lng": 8.6,
"heading": 90,
"course": 88.0,
"speed_kmh": 18.5,
"source": "local",
"last_seen": "2026-07-28T10:00:00Z",
"first_seen": "2026-01-15T08:30:00Z"
},
"_links": {
"self": "https://api.example.com/v1/vessels/211234560"
}
}

Error Responses:

StatusDescription
401Unauthorized — invalid or missing token
403Forbidden — missing ais:read permission
404Vessel not found in either Redis or the registry

Get Vessel Static History​

Paginated static-data change-log for a single MMSI, sourced from the append-only "VesselStaticHistory" TimescaleDB hypertable (DESC by recorded_at). One row per real static-data change (rename, dimension correction, ship-type change) — diffed against the registry so API restarts don't create duplicate rows. This is not a position/track endpoint; it never contains lat/lng. Shown on the webapp vessel detail page (/ais/vessels/{mmsi}) as the "Static History" table.

GET /v1/vessels/{mmsi}/history?page=1&limit=20
Authorization: Bearer YOUR_TOKEN

Path Parameters:

ParameterTypeDescription
mmsiintegerVessel MMSI

Query Parameters:

ParameterTypeDefaultDescription
pageinteger1Page number (1-indexed)
limitinteger20Items per page

VesselHistoryView (REST, snake_case) / GqlVesselHistoryEntry (GraphQL, camelCase):

REST field (snake_case)GraphQL field (camelCase)TypeDescription
ididUUIDHistory entry ID
mmsimmsiintegerVessel MMSI
namenamestring | nullVessel name at the time of the change
callsigncallsignstring | null
ship_typeshipTypeCodeinteger | nullNumeric ITU ship-type code. Note: the REST field is named ship_type (matching VesselHistoryView), while GraphQL names it shipTypeCode — an intentional naming difference between the two surfaces for this endpoint only
lengthlengthinteger | nullMeters
beambeaminteger | nullMeters
sourcesourcestringData source identifier
device_iddeviceIdstring | nullReporting device ID, if known
recorded_atrecordedAttimestampISO 8601. Vessel/event time of the change, not wall-clock write time

Response:

{
"data": [
{
"id": "32b2226c-18f3-452e-9d1f-fab5128110df",
"mmsi": 211234560,
"name": "MS RENA",
"callsign": "DABC",
"ship_type": 70,
"length": 100,
"beam": 20,
"source": "local",
"device_id": null,
"recorded_at": "2026-07-28T10:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"total_pages": 1
}
}

Error Responses:

StatusDescription
400Invalid MMSI
401Unauthorized — invalid or missing token
403Forbidden — missing ais:read permission

An unknown or never-changed MMSI returns an empty page (data: [], total: 0) — not a 404.

GraphQL API​

Query: vessels​

Paginated vessel directory.

query {
vessels(filter: { name: "rena", shipTypeCode: 70 }, page: 1, limit: 20) {
data {
id
mmsi
name
callsign
shipType
navStatus
length
beam
lat
lng
heading
course
speedKmh
source
lastSeen
firstSeen
}
total
page
limit
totalPages
}
}

VesselFilterInput:

FieldTypeDescription
nameStringCase-insensitive substring match on vessel name
shipTypeCodeIntExact numeric ITU ship-type code
shipTypeCodeMinIntInclusive lower bound of a ship-type code range (base-category filter, e.g. Cargo = 70)
shipTypeCodeMaxIntInclusive upper bound of the ship-type code range (e.g. Cargo = 79). Combined with shipTypeCodeMin this selects a whole category; the backend AND-combines all filters

The REST endpoint GET /v1/vessels mirrors these as query params ship_type_code_min / ship_type_code_max (alongside the exact ship_type). The backend web console's AIS directory uses this range filter to offer a ship-type category dropdown (Cargo, Tanker, Sailing, Tug, …).

Query: vessel​

Vessel detail by MMSI. Returns null if the vessel is in neither Redis nor the registry.

query {
vessel(mmsi: 211234560) {
id
mmsi
name
lat
lng
navStatus
speedKmh
lastSeen
firstSeen
}
}

Query: vesselHistory​

Paginated static-data change-log for a single MMSI. See Get Vessel Static History above for the field mapping and semantics — the GraphQL query is a straight mirror of the REST endpoint.

query {
vesselHistory(mmsi: 211234560, page: 1, limit: 20) {
data {
id
mmsi
name
callsign
shipTypeCode
length
beam
source
deviceId
recordedAt
}
total
page
limit
totalPages
}
}

vesselHistory(mmsi: Int!, page: Int! = 1, limit: Int! = 20): PaginatedVesselHistory! — PaginatedVesselHistory mirrors PaginatedVessel (data, total, page, limit, totalPages).

Required Permission​

The REST endpoints and the GraphQL queries (including vesselHistory) require ais:read on the caller's auth context. There is no separate write surface exposed here — vessel data is written internally by the AIS ingest pipeline, not through this API.

See Also​

  • WebSocket API — the "vessels" channel broadcasts live position updates (the same data that feeds the Redis hot cache used by GET /v1/vessels/{mmsi})
  • AIS Vessel Tracking — architecture overview, ingest pipeline, and the webapp detail page that renders this history