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.
- Open aisfleet.com/stations, or My Stations in the account menu.
- Choose Add a station, then An AIS receiver.
- 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:
station.positionin every report; or- the boat's own target, marked
"own_ship": true. When a report has nostation.position, that target's position becomes the station's, but only if itstimeis under ten minutes old and newer than the position already held.
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:
!AIVDM(also!BSVDM,!ABVDM): what the receiver hears. Message types 1, 2, 3, 5, 9, 18, 19, 21, 24 and 27 are used; a message split over two sentences is reassembled from consecutive fragments.!AIVDO: the boat the receiver is aboard. That boat is handled like any other target, and her position becomes the station's own.$--RMCand$--GGAwith a valid fix set the station's position. Every other$sentence (depth, wind, the boat's own compass heading) is ignored.- A position within 0.1° of 0°, 0°, from a GPS sentence or an AIS message, is treated as no position. A GPS or own-ship fix is used for ten minutes after it arrives; if no newer one follows, the station keeps the last position, dated when it arrived.
- IEC 61162-1 tag blocks (
\s:...,c:...*hh\) in front of a sentence are accepted; a plausiblec:receiver timestamp (unix seconds, not in the future, not older than six hours) is used as the report time, so a dispatcher replaying a backlog does not present old fixes as new.
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:
- it has moved more than about 100 m; or
- you have learned something new about it (a name arrived, a destination or ETA changed); or
- you still hear it and have said nothing about it for about five minutes, so its coverage receipt stays current. Your own map draws what your stations heard in the last few minutes, so a moored boat you re-send only every few hours reads as "left" there.
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
- Times,
etaincluded, in epoch milliseconds; speed in knots; course and heading in degrees; rate of turn in degrees per minute - Only changed targets per push; position included whenever known, with the speed, course, heading, rate of turn and navigational status that came with it
- A moving station's position in every report (
station.position, or the boat's own target markedown_ship) - Positions at 0°, 0° omitted; they are treated as no position
- AIS "not available" values omitted rather than sent as numbers
- 200 drops the batch; nothing is queued for replay
- 401 stops the client and surfaces a message; it does not retry
next_push_after_secondsrespectedclock_adjustedsurfaced to whoever runs the device- The token kept out of anything shared or published, a repository for instance
Questions: [email protected].