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, 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
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_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; 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.

LimitValueWhen exceeded
Concurrent connections per accountby plan; /v0 and /v1 count togethererror too_many_connections and Close 4005 for the new connection; open ones stay
Subscriptions per connection4error subscription_too_large and Close 4014 at once; unsubscribe first or reuse an id
Boxes50 per subscription, 80 per connectionerror subscription_too_large; Close 4014 if it is the connection's second mistake
MMSI per subscription500error subscription_too_large; Close 4014 if it is the connection's second mistake
Distinct MMSI per accountby plan, limits.mmsi_per_accounterror mmsi_limit_reached; never a close
Boxes and their summed area per account, all connectionsby 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 behindmore than 1,000 lost frames in one 60-second window; an overwritten frame counts as lostnotice 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