[{"data":1,"prerenderedAt":689},["ShallowReactive",2],{"docs-en-\u002Fdocs\u002Fstream":3},{"id":4,"title":5,"body":6,"description":682,"extension":683,"meta":684,"navigation":81,"path":685,"seo":686,"stem":687,"__hash__":688},"docs_en\u002Fdocs\u002Fstream\u002Findex.md","Stream documentation",{"type":7,"value":8,"toc":673},"minimark",[9,13,17,22,25,29,37,47,50,54,61,253,257,264,300,304,307,409,435,448,452,468,631,639,642,646,669],[10,11,5],"h1",{"id":12},"stream-documentation",[14,15,16],"p",{},"Everything you need to tap the live stream: the endpoint, the frames you get,\nand the limits that apply.",[18,19,21],"h2",{"id":20},"the-key-belongs-on-your-server","The key belongs on your server",[14,23,24],{},"Never put it in code a browser loads, and never in a URL. Anyone opening the\npage would read it, and query strings additionally end up in proxy logs.\nConnect from a server you control and pass the data on from there.",[18,26,28],{"id":27},"endpoint","Endpoint",[14,30,31,32,36],{},"One WebSocket connection, the key in the Authorization header. The service\nsends a ",[33,34,35],"code",{},"welcome"," frame immediately after connecting — it carries your\nsubscription limits and the attribution each source requires.",[38,39,44],"pre",{"className":40,"code":42,"language":43},[41],"language-text","wss:\u002F\u002Fstream.pelyr.com\u002Fv1\u002Fstream\nAuthorization: Bearer \u003Cyour key>\n","text",[33,45,42],{"__ignoreMap":46},"",[14,48,49],{},"A key in the query string is rejected with HTTP 400 rather than accepted\nquietly.",[18,51,53],{"id":52},"a-running-stream-in-thirty-lines","A running stream in thirty lines",[14,55,56,57,60],{},"Python with the ",[33,58,59],{},"websockets"," library. The key comes from the environment,\nnot from the source.",[38,62,66],{"className":63,"code":64,"language":65,"meta":46,"style":46},"language-python shiki shiki-themes github-light github-dark","import asyncio, json, os, websockets\n\nURL = \"wss:\u002F\u002Fstream.pelyr.com\u002Fv1\u002Fstream\"\nHEADERS = {\"Authorization\": \"Bearer \" + os.environ[\"PELYR_KEY\"]}\n\nasync def main():\n    async with websockets.connect(URL, additional_headers=HEADERS) as ws:\n        async for raw in ws:\n            frame = json.loads(raw)\n\n            if frame[\"type\"] == \"welcome\":\n                # Third-party credits must be visible to the end user; the Pelyr\n                # credit where section 3.1 of the Pelyr Data Licence requires it.\n                for q in frame[\"sources\"]:\n                    print(q[\"attribution\"])\n                await ws.send(json.dumps({\n                    \"type\": \"subscribe\",\n                    \"id\": \"baltic\",\n                    \"bbox\": [{\"west\": 18, \"south\": 59, \"east\": 26, \"north\": 66}],\n                    \"fields\": \"position\",\n                }))\n\n            elif frame[\"type\"] == \"position\":\n                d = frame[\"data\"]\n                print(d[\"mmsi\"], d[\"lat\"], d[\"lon\"], d[\"sog\"])\n\ntry:\n    asyncio.run(main())\nexcept websockets.ConnectionClosed as closed:\n    # 4012 means redeploy -- come back with 5-30 s of jitter.\n    print(\"closed:\", closed.code, closed.reason)\n","python",[33,67,68,76,83,89,95,100,106,112,118,124,129,135,141,147,153,159,165,171,177,183,189,195,200,206,212,218,223,229,235,241,247],{"__ignoreMap":46},[69,70,73],"span",{"class":71,"line":72},"line",1,[69,74,75],{},"import asyncio, json, os, websockets\n",[69,77,79],{"class":71,"line":78},2,[69,80,82],{"emptyLinePlaceholder":81},true,"\n",[69,84,86],{"class":71,"line":85},3,[69,87,88],{},"URL = \"wss:\u002F\u002Fstream.pelyr.com\u002Fv1\u002Fstream\"\n",[69,90,92],{"class":71,"line":91},4,[69,93,94],{},"HEADERS = {\"Authorization\": \"Bearer \" + os.environ[\"PELYR_KEY\"]}\n",[69,96,98],{"class":71,"line":97},5,[69,99,82],{"emptyLinePlaceholder":81},[69,101,103],{"class":71,"line":102},6,[69,104,105],{},"async def main():\n",[69,107,109],{"class":71,"line":108},7,[69,110,111],{},"    async with websockets.connect(URL, additional_headers=HEADERS) as ws:\n",[69,113,115],{"class":71,"line":114},8,[69,116,117],{},"        async for raw in ws:\n",[69,119,121],{"class":71,"line":120},9,[69,122,123],{},"            frame = json.loads(raw)\n",[69,125,127],{"class":71,"line":126},10,[69,128,82],{"emptyLinePlaceholder":81},[69,130,132],{"class":71,"line":131},11,[69,133,134],{},"            if frame[\"type\"] == \"welcome\":\n",[69,136,138],{"class":71,"line":137},12,[69,139,140],{},"                # Third-party credits must be visible to the end user; the Pelyr\n",[69,142,144],{"class":71,"line":143},13,[69,145,146],{},"                # credit where section 3.1 of the Pelyr Data Licence requires it.\n",[69,148,150],{"class":71,"line":149},14,[69,151,152],{},"                for q in frame[\"sources\"]:\n",[69,154,156],{"class":71,"line":155},15,[69,157,158],{},"                    print(q[\"attribution\"])\n",[69,160,162],{"class":71,"line":161},16,[69,163,164],{},"                await ws.send(json.dumps({\n",[69,166,168],{"class":71,"line":167},17,[69,169,170],{},"                    \"type\": \"subscribe\",\n",[69,172,174],{"class":71,"line":173},18,[69,175,176],{},"                    \"id\": \"baltic\",\n",[69,178,180],{"class":71,"line":179},19,[69,181,182],{},"                    \"bbox\": [{\"west\": 18, \"south\": 59, \"east\": 26, \"north\": 66}],\n",[69,184,186],{"class":71,"line":185},20,[69,187,188],{},"                    \"fields\": \"position\",\n",[69,190,192],{"class":71,"line":191},21,[69,193,194],{},"                }))\n",[69,196,198],{"class":71,"line":197},22,[69,199,82],{"emptyLinePlaceholder":81},[69,201,203],{"class":71,"line":202},23,[69,204,205],{},"            elif frame[\"type\"] == \"position\":\n",[69,207,209],{"class":71,"line":208},24,[69,210,211],{},"                d = frame[\"data\"]\n",[69,213,215],{"class":71,"line":214},25,[69,216,217],{},"                print(d[\"mmsi\"], d[\"lat\"], d[\"lon\"], d[\"sog\"])\n",[69,219,221],{"class":71,"line":220},26,[69,222,82],{"emptyLinePlaceholder":81},[69,224,226],{"class":71,"line":225},27,[69,227,228],{},"try:\n",[69,230,232],{"class":71,"line":231},28,[69,233,234],{},"    asyncio.run(main())\n",[69,236,238],{"class":71,"line":237},29,[69,239,240],{},"except websockets.ConnectionClosed as closed:\n",[69,242,244],{"class":71,"line":243},30,[69,245,246],{},"    # 4012 means redeploy -- come back with 5-30 s of jitter.\n",[69,248,250],{"class":71,"line":249},31,[69,251,252],{},"    print(\"closed:\", closed.code, closed.reason)\n",[18,254,256],{"id":255},"what-you-receive","What you receive",[14,258,259,260,263],{},"Every frame is one JSON object, and ",[33,261,262],{},"type"," always comes first — you can\nbranch on it without parsing the rest.",[265,266,267,276,284,292],"ul",{},[268,269,270,275],"li",{},[271,272,273],"strong",{},[33,274,35],{}," — immediately, unprompted",[268,277,278,283],{},[271,279,280],{},[33,281,282],{},"subscribed"," — what actually applies, not what you asked for",[268,285,286,291],{},[271,287,288],{},[33,289,290],{},"position"," — the ships",[268,293,294,299],{},[271,295,296],{},[33,297,298],{},"heartbeat"," — every 20 seconds, even in silence",[18,301,303],{"id":302},"choosing-what-arrives","Choosing what arrives",[14,305,306],{},"Up to four subscriptions per connection. Within one subscription the filters\nare ANDed, several boxes are ORed. A message matching two subscriptions\nstill arrives once.",[308,309,310,326],"table",{},[311,312,313],"thead",{},[314,315,316,320,323],"tr",{},[317,318,319],"th",{},"Field",[317,321,322],{},"Default",[317,324,325],{},"Meaning",[327,328,329,343,355,367,385],"tbody",{},[314,330,331,337,340],{},[332,333,334],"td",{},[33,335,336],{},"bbox",[332,338,339],{},"—",[332,341,342],{},"Up to 50 boxes per subscription, at most 80 per connection",[314,344,345,350,352],{},[332,346,347],{},[33,348,349],{},"mmsi",[332,351,339],{},[332,353,354],{},"Up to 500 specific vessels",[314,356,357,362,364],{},[332,358,359],{},[33,360,361],{},"msg_types",[332,363,339],{},[332,365,366],{},"AIS message types",[314,368,369,374,379],{},[332,370,371],{},[33,372,373],{},"fields",[332,375,376],{},[33,377,378],{},"\"full\"",[332,380,381,384],{},[33,382,383],{},"\"position\""," returns eight fields instead of all",[314,386,387,392,397],{},[332,388,389],{},[33,390,391],{},"include_positionless",[332,393,394],{},[33,395,396],{},"false",[332,398,399,400,405,406,408],{},"Also messages without a position: names, destinations. ",[271,401,402,403],{},"Only works on a subscription with no ",[33,404,336],{}," — 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 ",[33,407,349],{}," list",[14,410,411,412,415,416,415,419,422,423,426,427,430,431,434],{},"The edges are called ",[33,413,414],{},"west",", ",[33,417,418],{},"south",[33,420,421],{},"east"," and ",[33,424,425],{},"north",", spelled out rather\nthan ",[33,428,429],{},"[lat, lon]",". This is the most common early mistake, because the order\nis exactly reversed relative to GeoJSON. ",[33,432,433],{},"west > east"," is not an error but a\nbox crossing the antimeridian.",[14,436,437,438,440,441,443,444,447],{},"On the Free and Fleet plans the stream does not cover the whole world: a\nsubscription needs a ",[33,439,336],{}," or an ",[33,442,349],{}," list, and the boxes of all your\nsubscriptions together are capped in area, on Free also in number (see\nbelow). A box's area is its real area on the earth in km², taken on a sphere\nof radius 6,371 km, so a box of the same degrees is smaller towards the\npoles; for a box across the antimeridian the width is ",[33,445,446],{},"360 - west + east",".\nOverlapping boxes count in full.",[18,449,451],{"id":450},"limits","Limits",[14,453,454,455,457,458,461,462,467],{},"The ",[33,456,35],{}," frame carries your subscription limits, your account's MMSI,\nbox and area limits and the output-rate and volume fields (all ",[33,459,460],{},"0",", meaning no limit). How\nmany connections and API keys your account may hold is set by your plan; the\nvalues are listed at ",[463,464,466],"a",{"href":465},"\u002Fpricing","pricing",". The rule: the first mistake on a\nconnection is a message, the next one disconnects — however much later it\ncomes, because accepted subscribes do not reset the count. The first typo\ncosts you the change, not the connection.",[308,469,470,483],{},[311,471,472],{},[314,473,474,477,480],{},[317,475,476],{},"Limit",[317,478,479],{},"Value",[317,481,482],{},"When exceeded",[327,484,485,510,529,544,558,577,604,621],{},[314,486,487,490,500],{},[332,488,489],{},"Concurrent connections per account",[332,491,492,493,422,496,499],{},"by plan; ",[33,494,495],{},"\u002Fv0",[33,497,498],{},"\u002Fv1"," count together",[332,501,502,505,506,509],{},[33,503,504],{},"error"," ",[33,507,508],{},"too_many_connections"," and Close 4005 for the new connection; open ones stay",[314,511,512,515,518],{},[332,513,514],{},"Subscriptions per connection",[332,516,517],{},"4",[332,519,520,505,522,525,526],{},[33,521,504],{},[33,523,524],{},"subscription_too_large"," and Close 4014 at once; unsubscribe first or reuse an ",[33,527,528],{},"id",[314,530,531,534,537],{},[332,532,533],{},"Boxes",[332,535,536],{},"50 per subscription, 80 per connection",[332,538,539,505,541,543],{},[33,540,504],{},[33,542,524],{},"; Close 4014 if it is the connection's second mistake",[314,545,546,549,552],{},[332,547,548],{},"MMSI per subscription",[332,550,551],{},"500",[332,553,554,505,556,543],{},[33,555,504],{},[33,557,524],{},[314,559,560,563,569],{},[332,561,562],{},"Distinct MMSI per account",[332,564,565,566],{},"by plan, ",[33,567,568],{},"limits.mmsi_per_account",[332,570,571,505,573,576],{},[33,572,504],{},[33,574,575],{},"mmsi_limit_reached","; never a close",[314,578,579,582,591],{},[332,580,581],{},"Boxes and their summed area per account, all connections",[332,583,565,584,422,587,590],{},[33,585,586],{},"limits.bbox_per_account",[33,588,589],{},"limits.bbox_area_km2_per_account","; Free: 10 boxes, 3,500,000 km²; Fleet: any number of boxes, 15,000,000 km²",[332,592,593,505,595,598,599,422,601,603],{},[33,594,504],{},[33,596,597],{},"bbox_limit_reached",", also for a subscription without ",[33,600,336],{},[33,602,349],{}," while an area limit applies; never a close",[314,605,606,609,612],{},[332,607,608],{},"Falling behind",[332,610,611],{},"more than 1,000 lost frames in one 60-second window; an overwritten frame counts as lost",[332,613,614,505,617,620],{},[33,615,616],{},"notice",[33,618,619],{},"slow_consumer_warning"," at 500, then Close 4008",[314,622,623,626,628],{},[332,624,625],{},"Key revoked",[332,627,339],{},[332,629,630],{},"Close 4011 on open connections, usually at once and normally at the next check, every 60 seconds",[14,632,633,634,638],{},"API keys are shared with the HTTPS API; their limits are described there:\n",[463,635,637],{"href":636},"\u002Fen\u002Fdocs\u002Fapi#api-keys","API keys",".",[14,640,641],{},"Close 4012 means we redeployed. Come back after 5 to 30 seconds of random\ndelay, not immediately — otherwise everyone returns at once.",[18,643,645],{"id":644},"next","Next",[265,647,648,655,662],{},[268,649,650,654],{},[463,651,653],{"href":652},"\u002Fen\u002Fdocs\u002Fstream\u002Fprotocol","Protocol reference"," — Every frame, every field, all fourteen close codes.",[268,656,657,661],{},[463,658,660],{"href":659},"\u002Fen\u002Fdocs\u002Fstream\u002Fexamples","Example clients"," — Python and Go, with reconnection and loss detection.",[268,663,664,668],{},[463,665,667],{"href":666},"\u002Fen\u002Fdocs\u002Fstream\u002Faisstream","aisstream.io compatibility"," — Moving an existing aisstream.io client to Pelyr OPEN-AIS.",[670,671,672],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":78,"depth":78,"links":674},[675,676,677,678,679,680,681],{"id":20,"depth":78,"text":21},{"id":27,"depth":78,"text":28},{"id":52,"depth":78,"text":53},{"id":255,"depth":78,"text":256},{"id":302,"depth":78,"text":303},{"id":450,"depth":78,"text":451},{"id":644,"depth":78,"text":645},"Getting started with the Pelyr OPEN-AIS live AIS feed.","md",{},"\u002Fdocs\u002Fstream",{"title":5,"description":682},"docs\u002Fstream\u002Findex","2vJUidp0pzVBWDDOJEJnkEFtxeh_np_u99RQriQT9Zs",1793034560956]