A station is one AIS receiver whose output you have chosen to contribute. Its reports join the vessel data drawn on the AIS Fleet map, feed the departure and arrival notices members get for boats they follow, and are included in the data licensed to partners. Section 3 of the Terms is the permission that covers this, and it continues for reports already sent.

Sharing is free on every plan, with no limit on how many stations you run, and a station is private to you: nobody else sees its name, its position, or that it exists, and nothing says which station reported a vessel.

Base URL: https://aisfleet.com

Getting a token

Every request carries a bearer token that belongs to the station, not to a person's sign-in.

  1. Open aisfleet.com/stations, or My Stations in the account menu.
  2. Choose Add a station, then An AIS receiver.
  3. Under Connection, choose API token, name the station if you like, and choose Add receiver.

The token appears at once, with the address to send reports to, and both stay on the station's page under Settings (the gear on the station's page) whenever you need them again, the way a UDP station's port does. A station that already streams over UDP gets a token from the same page with Add a token; it then reports either way, and both count for the same station. The AIS Fleet app pairs its own token when sharing is turned on, and support can pair a station for you on request.

If the token may have leaked, or you are replacing the software, choose New token on the station's page. The station keeps its name and its history; the previous token stops working the moment the new one exists, so software still sending it is refused until it has the new one.

Every request:

Authorization: Bearer sls_<prefix>_<secret>
Content-Type: application/json

The <prefix> is the part of the token a station's row shows on My Stations. It is not secret, and it names the station in a message to support without handing over the token.

Sending reports

POST /api/station/v1/report

{
"station": {
"position": {"latitude": 43.5081, "longitude": 16.4402},
"software": "sky-pi 1.2"
},
"targets": [
{
"mmsi": 238123456,
"time": 1755600000000,
"position": {"latitude": 43.5102, "longitude": 16.4421},
"speed": 6.2,
"course": 183.4,
"heading": 181,
"rate_of_turn": -4.5,
"nav_status": 0,
"name": "SEA FEVER",
"call_sign": "9A1234",
"imo": 9123456,
"ship_type": 37,
"ais_class": "A",
"length": 12.5,
"beam": 4.1,
"draught": 1.8,
"destination": "SPLIT",
"eta": 1755624600000
}
]
}

station describes the receiver itself, and targets lists what it heard. Both are optional: a report can carry the station's position alone, as described under Station position.

Units

Knots, degrees true, degrees per minute, metres, decimal degrees, epoch milliseconds UTC. Keys are snake_case.

If you also implement Saillogger's collector API (/api/v2/collector/), note that this one is deliberately different: that API speaks Signal K and SI units, this one speaks AIS. Every field here is an AIS message field, every AIS decoder already produces knots and degrees, and speed is not a display value on our side: it drives the state machine that decides whether a boat is under way, so a unit conversion error there can send a follower a departure notice for a boat that never moved.

Target fields

Field Type Required Meaning
mmsi integer yes 9 digits, 100000000-999999999. Anything else is rejected
time integer yes Epoch milliseconds UTC of the newest message you merged for this target
position object no {"latitude": ..., "longitude": ...}, decimal degrees. A position within 0.1° of 0°, 0° is treated as no position
speed number no Speed over ground in knots. Omit if unknown
course number no Course over ground in degrees true, 0 to <360
heading number no True heading in degrees, 0 to <360: the way the bow points, which differs from course at anchor or across a current
rate_of_turn number no Rate of turn in degrees per minute, -720 to 720, positive when turning to starboard. The decoded rate, not the AIS code
nav_status integer no AIS navigational status, 0 to 14, as the transponder sends it: 0 under way using engine, 1 at anchor, 5 moored, 8 under way sailing, and the others as ITU-R M.1371 defines them
name string no Vessel name, trimmed, up to 255 characters
call_sign string no Up to 20 characters
flag string no ISO country code or country name, up to 50 characters
imo integer no IMO ship number. The check digit is validated
ship_type integer no Raw AIS ship type, 1 to 99
ais_class string no "A", "B" or "BASE"
length number no Overall length in metres, 0 < n <= 500
beam number no Beam in metres, 0 < n <= 150
draught number no Draught in metres, 0 < n <= 30
destination string no As broadcast, up to 255 characters
eta integer no Estimated time of arrival, epoch milliseconds UTC. AIS sends it without a year: use the year that puts it nearest eta_time, or time when eta_time is omitted. More than a year from that report is ignored
eta_time integer no Epoch milliseconds UTC of the message that supplied eta. Keep this time with a cached ETA when newer position messages arrive. Omit to use time
repeated boolean no true when the position came through a repeater (the AIS repeat indicator was above zero). Still recorded as a hearing, but never credited to the station's coverage range
own_ship boolean no true for the boat the receiver is aboard, the JSON form of !AIVDO. That boat is handled like any other target, and her position can become the station's, as described under Station position

Send AIS "not available" values as absent, not as numbers. AIS encodes "not available" as ordinary-looking values: speed 102.3, course 360, heading 511, navigational status 15, dimensions and draught 0, ship type 0, latitude 91 and longitude 181. We filter all of those, and omitting them is clearer and smaller. Two are the client's to leave out, because once decoded no number is left to filter: an ETA whose month or day is 0 was never set, and a rate-of-turn code of -128 (not available), 127 or -127 (turning, rate unknown) carries no rate.

Unknown fields are ignored. You can send extra keys safely, and we may add fields without breaking your client.

When supplied, eta_time must be valid, no later than time, and not in the future. An unusable timestamp leaves the stored ETA unchanged; the other fields are processed normally. An ignored ETA does not count as changed data.

Station fields

Field Type Meaning
position object Where the receiver is listening from, in decimal degrees: from the receiver's own GPS or a fixed shore position, not from a phone's location services. See Station position
software string Self-declared client, for support. Up to 100 characters

Station position

The owner's map shows the station at this position, with how far it reaches, and the distance to each boat the station hears is measured from there. Only the owner sees it.

A fixed receiver sends station.position in every report, or its owner enters the position once on the station's page.

A receiver aboard a boat that moves reports its position as it goes, in either of two ways:

If reports overlap, the station keeps the newer position. A position entered by the owner stays in place.

{
"targets": [
{"mmsi": 238123456, "time": 1755600000000, "own_ship": true,
"position": {"latitude": 43.5102, "longitude": 16.4421},
"speed": 6.2, "course": 183.4},
{"mmsi": 244987654, "time": 1755599990000,
"position": {"latitude": 43.4210, "longitude": 16.3012}}
]
}

When there is nothing new to send, with nothing in range for instance, a report can carry the station's position alone, with no targets:

{"station": {"position": {"latitude": 43.5081, "longitude": 16.4402}}}

Such a report keeps the station Live, so no outage notice goes out, and keeps its position current; it is not counted as hearing a boat, so it does not change uptime.

Range is measured from a position the owner entered, if there is one; otherwise from the station's position in this report (station.position or the own-ship target's); otherwise, when the report has none, from the last reported position under ten minutes old. A position within 0.1° of 0°, 0°, which a GPS can send when it cannot determine its position, is treated as no position, whether it is the station's or a target's. A position the owner entered on the station's page is kept until the owner clears it, and reports do not change it. The station object in each response carries the position held: position, position_updated (epoch milliseconds: when the report arrived, or the own-ship target's time, capped at receipt time) and position_source ("feed" for a reported position, "manual" for one the owner entered).

Or stream NMEA 0183 over UDP

If your receiver, multiplexer or software can forward NMEA 0183 over UDP, the JSON API is not needed at all. Add the station with UDP as its connection and point the forwarder at the host and port you are given. The server decodes and merges the stream, applies the rules under What to send, and how often for you, and pushes through the same ingest as a JSON report, once a minute. The member-facing steps, including the Signal K plugins, are under Share your AIS receiver.

Getting an address. On My Stations, choose Add a station, then An AIS receiver, and leave Connection on UDP. The page shows the host, data.aisfleet.com, and the station's port, and shows them again whenever the station is opened. Through the member API it is POST /api/v1/stations with "transport": "udp"; the response carries udp_host and udp_port inside station, and a null token.

What to send. Raw NMEA 0183 sentences, one or more per datagram, each ending in CR LF:

A boat reporting an unavailable or zeroed position does not add uptime credit until she reports a valid position again. Her last known position remains on record.

A sentence that fails its checksum, a datagram over 8 KB, and anything that is not NMEA is dropped and counted. The server never replies to a datagram.

Limits. 20,000 sentences a minute per station; the surplus is dropped until the next minute starts. A port is never changed in place: to move to a different one, remove the UDP address from the station's page and add one again, or remove the station. The Limits and What happens to what you send sections apply unchanged. A station without a token has nothing to rotate, so POST /api/v1/stations/<id>/rotate answers 400 for it until a token is added.

Checking it. The station's row on My Stations reads Live once data has arrived in the last ten minutes, with the number of boats heard in the last 24 hours. Data arrives within about a minute of the first datagram.

What to send, and how often

Push about once a minute. AIS reporting intervals run from 2 seconds for a fast Class A to 3 minutes for a Class B at anchor, so a one-minute cycle loses nothing a chart can show.

Send only what changed. Include a target when, since you last sent it:

For an anchorage full of moored boats, send a position-only line per boat every few minutes, and nothing in between.

Within a target, send the fields you hold and omit the rest. An omitted identity or voyage field, name through eta in the table above, means "no news" and never clears a stored value.

Position is the exception, and is not a delta. Include it on every report for a target whose position you know, even when it has not changed. It is what makes a report placeable, and the server reads it to decide whether the report is worth storing.

Motion travels with the position. speed, course, heading, rate_of_turn and nav_status describe the position report they arrive with, so send the ones you hold alongside every position. One that is left out reads as not known at that report's time, never as unchanged.

One target per MMSI per push. If a push carries the same MMSI twice, the newest wins and the other is discarded.

Keep a moving station's position current. A receiver aboard a boat that moves includes its position in every report, as station.position or as the boat's own target marked own_ship. When there is nothing new to send, it still sends its position alone at least every few minutes.

The response

{
"accepted": 128,
"rejected": 0,
"stale": 3,
"overflow": 0,
"clock_adjusted": 0,
"vessels_updated": 41,
"vessels_created": 2,
"vessel_writes_deferred": 0,
"max_targets_per_push": 2000,
"next_push_after_seconds": 60,
"station": {
"id": 12,
"name": "Masthead",
"prefix": "3f9a1c22",
"is_active": true,
"live": true,
"software": "sky-pi 1.2",
"position": {"latitude": 43.5081, "longitude": 16.4402},
"position_updated": 1755600060000,
"position_source": "feed",
"last_report": 1755600060000,
"report_count": 1440,
"target_count": 51233,
"last_target_count": 128,
"vessels_heard": null,
"vessels_heard_window_hours": 24
}
}
Field Meaning
accepted Targets recorded. This is the number that means "my station is contributing"
rejected Unusable targets, dropped. A value that stays above zero points to a client-side bug
stale Targets whose time was more than 6 hours old
overflow Targets past max_targets_per_push, not read
clock_adjusted Your clock is ahead of the correct time, so we stamped those reports at receipt. Worth surfacing to whoever runs the device: the data was recorded, and the clock is wrong
vessels_updated Existing vessels this push moved
vessels_created Vessels the network had never heard of
vessel_writes_deferred Accepted, but the vessel write was deferred to your next push. Not lost, no action needed
next_push_after_seconds How long to wait. A client that waits this long is never throttled

vessels_heard is null here (this response does not count it). GET /api/station/v1/status returns the same station object with it filled in, for a client that has just been configured and wants to check itself without sending data.

A 200 means the batch has been handled. Discard it. Do not retry a 200, and do not try to work out from the counts whether some targets need resending: an unusable target needs a fix in the client, and replaying the batch does not help. Per-target outcomes are counts, never errors, so one bad target can never fail a push carrying four hundred good ones.

Errors

Status Meaning What to do
400 Body is not an object, or targets is sent but is not a list A client-side error: correct the body rather than retrying it
401 Token missing, invalid, or the station was switched off or removed Stop sending. Retrying cannot help; the token on My Stations is the one to check
413 Body over 2 MB Split the batch
429 Faster than 10 pushes per minute Back off to next_push_after_seconds, or the Retry-After header
5xx A server error on our side Exponential backoff with jitter

Errors are {"error": "..."}, always that key.

Do not queue failed pushes for replay. AIS is a live picture: by the time a failed push could be retried it is stale, and your next cycle carries the current data. Keep track of what you last sent, so your deltas stay accurate across a failure, and discard the failed batch.

Limits

Limit Value
Push rate 10 per minute, per station
Targets per push 2000
Body 2 MB
Report age Older than 6 hours is rejected as stale
Clock skew More than 10 minutes into the future is stamped at receipt

What happens to what you send

Every valid target is recorded as a coverage receipt for your station, whatever it is, including buoys, base stations and beacons.

Ship MMSIs additionally update the shared vessel record. Reports compete on report time with every other source, so the newest report wins regardless of who sent it, and a position first-hand from a Saillogger boat's own equipment takes priority over external sources for a short window after it arrives.

Non-ship transmitters (aids to navigation, base stations, SAR aircraft, distress beacons) never create a vessel record. The map draws vessels, and a buoy drawn as traffic would put false information on the chart. If one already has a record from another source, your reports refresh it.

Coverage receipts for a station are kept for a rolling window and expire when nothing re-hears the transmitter. Vessel data does not expire with them: once contributed, a position is part of what the network knows.

Managing stations from the app and the web

These need a member session, not a station token, and exist for the app and the My Stations page. A device implementer does not call them.

Route Does
GET /api/v1/stations The member's stations, with 24 h heard-counts and, for a token station, its token
POST /api/v1/stations Pair one. Body: name, optional software, optional transport ("token", the default, or "udp"). The token is in the response, top-level and inside station; null for a UDP station, whose station carries udp_host and udp_port instead
PATCH /api/v1/stations/<id> Rename, or switch on and off
POST /api/v1/stations/<id>/rotate New token, same station and history. 400 for a station without a token
POST /api/v1/stations/<id>/udp Give the station a UDP address as well. 400 if it has one or is a phone, 503 while UDP is not available
DELETE /api/v1/stations/<id>/udp Give the UDP address up. 400 when it is the station's only way in
POST /api/v1/stations/<id>/token Give the station a token as well; the token is in the response, top-level and inside station. 400 if it has one
DELETE /api/v1/stations/<id>/token Close the token door. 400 when it is the only way in, or for a phone
DELETE /api/v1/stations/<id> Remove it and its receipts

Every station object carries udp_host and udp_port (null without a UDP address), token_enabled (whether a token opens it), token (null without a token, and for a station paired before the token was kept, whose device alone holds it), transport ("udp" when the station has a UDP address, else "token") and kind: "phone" for the AIS Fleet app sharing the receiver aboard, "udp" for a station with a UDP address, "token" for any other software paired against this API. A station can have both a UDP address and a token; clients that predate a field ignore it.

Checklist before you ship

Questions: [email protected].