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 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":
# Required: attribution must be visible to the end user.
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 |
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.
Limits
The values that apply to you are in the welcome frame. The rule: the first
mistake is a message, a repeated one disconnects. A typo costs you the
change, not the connection.
| Limit | Value | When exceeded |
|---|---|---|
| Concurrent connections | 2 | Close 4005 |
| Subscriptions per connection | 4 | error |
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.