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, unprompted
  • subscribed — what actually applies, not what you asked for
  • position — the ships
  • heartbeat — 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.

FieldDefaultMeaning
bboxUp to 50 boxes per subscription, at most 80 per connection
mmsiUp to 500 specific vessels
msg_typesAIS message types
fields"full""position" returns eight fields instead of all
include_positionlessfalseAlso 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.

LimitValueWhen exceeded
Concurrent connections2Close 4005
Subscriptions per connection4error

Close 4012 means we redeployed. Come back after 5 to 30 seconds of random delay, not immediately — otherwise everyone returns at once.

Next