Stream documentation
Everything you need to tap the live stream: the endpoint, the frames you get, and the limits that apply.
The key belongs on your server
Never put it in code a browser loads, and never in a URL. Anyone opening the page would read it, and query strings additionally end up in proxy logs. Connect from a server you control and pass the data on from there.
Endpoint
One WebSocket connection, the key in the Authorization header. The service
sends a welcome frame immediately after connecting — it carries your
subscription limits and the attribution each source requires.
wss://stream.pelyr.com/v1/stream
Authorization: Bearer <your key>
A key in the query string is rejected with HTTP 400 rather than accepted quietly.
A running stream in thirty lines
Python with the websockets library. The key comes from the environment,
not from the source.
import asyncio, json, os, websockets
URL = "wss://stream.pelyr.com/v1/stream"
HEADERS = {"Authorization": "Bearer " + os.environ["PELYR_KEY"]}
async def main():
async with websockets.connect(URL, additional_headers=HEADERS) as ws:
async for raw in ws:
frame = json.loads(raw)
if frame["type"] == "welcome":
# Third-party credits must be visible to the end user; the Pelyr
# credit where section 3.1 of the Pelyr Data Licence requires it.
for q in frame["sources"]:
print(q["attribution"])
await ws.send(json.dumps({
"type": "subscribe",
"id": "baltic",
"bbox": [{"west": 18, "south": 59, "east": 26, "north": 66}],
"fields": "position",
}))
elif frame["type"] == "position":
d = frame["data"]
print(d["mmsi"], d["lat"], d["lon"], d["sog"])
try:
asyncio.run(main())
except websockets.ConnectionClosed as closed:
# 4012 means redeploy -- come back with 5-30 s of jitter.
print("closed:", closed.code, closed.reason)
What you receive
Every frame is one JSON object, and type always comes first — you can
branch on it without parsing the rest.
welcome— immediately, unpromptedsubscribed— what actually applies, not what you asked forposition— the shipsheartbeat— every 20 seconds, even in silence
Choosing what arrives
Up to four subscriptions per connection. Within one subscription the filters are ANDed, several boxes are ORed. A message matching two subscriptions still arrives once.
| Field | Default | Meaning |
|---|---|---|
bbox | — | Up to 50 boxes per subscription, at most 80 per connection |
mmsi | — | Up to 500 specific vessels |
msg_types | — | AIS message types |
fields | "full" | "position" returns eight fields instead of all |
include_positionless | false | Also messages without a position: names, destinations. Only works on a subscription with no bbox — a message without coordinates cannot satisfy a box, so on a bbox subscription this flag does nothing. Use a second subscription for names; on Free and Fleet with an mmsi list |
The edges are called west, south, east and north, spelled out rather
than [lat, lon]. This is the most common early mistake, because the order
is exactly reversed relative to GeoJSON. west > east is not an error but a
box crossing the antimeridian.
On the Free and Fleet plans the stream does not cover the whole world: a
subscription needs a bbox or an mmsi list, and the boxes of all your
subscriptions together are capped in area, on Free also in number (see
below). A box's area is its real area on the earth in km², taken on a sphere
of radius 6,371 km, so a box of the same degrees is smaller towards the
poles; for a box across the antimeridian the width is 360 - west + east.
Overlapping boxes count in full.
Limits
The welcome frame carries your subscription limits, your account's MMSI,
box and area limits and the output-rate and volume fields (all 0, meaning no limit). How
many connections and API keys your account may hold is set by your plan; the
values are listed at pricing. The rule: the first mistake on a
connection is a message, the next one disconnects — however much later it
comes, because accepted subscribes do not reset the count. The first typo
costs you the change, not the connection.
| Limit | Value | When exceeded |
|---|---|---|
| Concurrent connections per account | by plan; /v0 and /v1 count together | error too_many_connections and Close 4005 for the new connection; open ones stay |
| Subscriptions per connection | 4 | error subscription_too_large and Close 4014 at once; unsubscribe first or reuse an id |
| Boxes | 50 per subscription, 80 per connection | error subscription_too_large; Close 4014 if it is the connection's second mistake |
| MMSI per subscription | 500 | error subscription_too_large; Close 4014 if it is the connection's second mistake |
| Distinct MMSI per account | by plan, limits.mmsi_per_account | error mmsi_limit_reached; never a close |
| Boxes and their summed area per account, all connections | by plan, limits.bbox_per_account and limits.bbox_area_km2_per_account; Free: 10 boxes, 3,500,000 km²; Fleet: any number of boxes, 15,000,000 km² | error bbox_limit_reached, also for a subscription without bbox and mmsi while an area limit applies; never a close |
| Falling behind | more than 1,000 lost frames in one 60-second window; an overwritten frame counts as lost | notice slow_consumer_warning at 500, then Close 4008 |
| Key revoked | — | Close 4011 on open connections, usually at once and normally at the next check, every 60 seconds |
API keys are shared with the HTTPS API; their limits are described there: API keys.
Close 4012 means we redeployed. Come back after 5 to 30 seconds of random delay, not immediately — otherwise everyone returns at once.
Next
- Protocol reference — Every frame, every field, all fourteen close codes.
- Example clients — Python and Go, with reconnection and loss detection.
- aisstream.io compatibility — Moving an existing aisstream.io client to Pelyr OPEN-AIS.