[{"data":1,"prerenderedAt":526},["ShallowReactive",2],{"docs-en-\u002Fdocs\u002Fstream":3},{"id":4,"title":5,"body":6,"description":519,"extension":520,"meta":521,"navigation":81,"path":522,"seo":523,"stem":524,"__hash__":525},"docs_en\u002Fdocs\u002Fstream\u002Findex.md","Stream documentation",{"type":7,"value":8,"toc":510},"minimark",[9,13,17,22,25,29,37,47,50,54,61,247,251,258,294,298,301,400,426,430,436,475,478,482,506],[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 limits\nand 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                # Required: attribution must be visible to the end user.\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,194,200,206,212,217,223,229,235,241],{"__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],{},"                # Required: attribution must be visible to the end user.\n",[69,142,144],{"class":71,"line":143},13,[69,145,146],{},"                for q in frame[\"sources\"]:\n",[69,148,150],{"class":71,"line":149},14,[69,151,152],{},"                    print(q[\"attribution\"])\n",[69,154,156],{"class":71,"line":155},15,[69,157,158],{},"                await ws.send(json.dumps({\n",[69,160,162],{"class":71,"line":161},16,[69,163,164],{},"                    \"type\": \"subscribe\",\n",[69,166,168],{"class":71,"line":167},17,[69,169,170],{},"                    \"id\": \"baltic\",\n",[69,172,174],{"class":71,"line":173},18,[69,175,176],{},"                    \"bbox\": [{\"west\": 18, \"south\": 59, \"east\": 26, \"north\": 66}],\n",[69,178,180],{"class":71,"line":179},19,[69,181,182],{},"                    \"fields\": \"position\",\n",[69,184,186],{"class":71,"line":185},20,[69,187,188],{},"                }))\n",[69,190,192],{"class":71,"line":191},21,[69,193,82],{"emptyLinePlaceholder":81},[69,195,197],{"class":71,"line":196},22,[69,198,199],{},"            elif frame[\"type\"] == \"position\":\n",[69,201,203],{"class":71,"line":202},23,[69,204,205],{},"                d = frame[\"data\"]\n",[69,207,209],{"class":71,"line":208},24,[69,210,211],{},"                print(d[\"mmsi\"], d[\"lat\"], d[\"lon\"], d[\"sog\"])\n",[69,213,215],{"class":71,"line":214},25,[69,216,82],{"emptyLinePlaceholder":81},[69,218,220],{"class":71,"line":219},26,[69,221,222],{},"try:\n",[69,224,226],{"class":71,"line":225},27,[69,227,228],{},"    asyncio.run(main())\n",[69,230,232],{"class":71,"line":231},28,[69,233,234],{},"except websockets.ConnectionClosed as closed:\n",[69,236,238],{"class":71,"line":237},29,[69,239,240],{},"    # 4012 means redeploy -- come back with 5-30 s of jitter.\n",[69,242,244],{"class":71,"line":243},30,[69,245,246],{},"    print(\"closed:\", closed.code, closed.reason)\n",[18,248,250],{"id":249},"what-you-receive","What you receive",[14,252,253,254,257],{},"Every frame is one JSON object, and ",[33,255,256],{},"type"," always comes first — you can\nbranch on it without parsing the rest.",[259,260,261,270,278,286],"ul",{},[262,263,264,269],"li",{},[265,266,267],"strong",{},[33,268,35],{}," — immediately, unprompted",[262,271,272,277],{},[265,273,274],{},[33,275,276],{},"subscribed"," — what actually applies, not what you asked for",[262,279,280,285],{},[265,281,282],{},[33,283,284],{},"position"," — the ships",[262,287,288,293],{},[265,289,290],{},[33,291,292],{},"heartbeat"," — every 20 seconds, even in silence",[18,295,297],{"id":296},"choosing-what-arrives","Choosing what arrives",[14,299,300],{},"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.",[302,303,304,320],"table",{},[305,306,307],"thead",{},[308,309,310,314,317],"tr",{},[311,312,313],"th",{},"Field",[311,315,316],{},"Default",[311,318,319],{},"Meaning",[321,322,323,337,349,361,379],"tbody",{},[308,324,325,331,334],{},[326,327,328],"td",{},[33,329,330],{},"bbox",[326,332,333],{},"—",[326,335,336],{},"Up to 50 boxes per subscription, at most 80 per connection",[308,338,339,344,346],{},[326,340,341],{},[33,342,343],{},"mmsi",[326,345,333],{},[326,347,348],{},"Up to 500 specific vessels",[308,350,351,356,358],{},[326,352,353],{},[33,354,355],{},"msg_types",[326,357,333],{},[326,359,360],{},"AIS message types",[308,362,363,368,373],{},[326,364,365],{},[33,366,367],{},"fields",[326,369,370],{},[33,371,372],{},"\"full\"",[326,374,375,378],{},[33,376,377],{},"\"position\""," returns eight fields instead of all",[308,380,381,386,391],{},[326,382,383],{},[33,384,385],{},"include_positionless",[326,387,388],{},[33,389,390],{},"false",[326,392,393,394,399],{},"Also messages without a position: names, destinations. ",[265,395,396,397],{},"Only works on a subscription with no ",[33,398,330],{}," — a message without coordinates cannot satisfy a box, so on a bbox subscription this flag does nothing. Use a second subscription for names",[14,401,402,403,406,407,406,410,413,414,417,418,421,422,425],{},"The edges are called ",[33,404,405],{},"west",", ",[33,408,409],{},"south",[33,411,412],{},"east"," and ",[33,415,416],{},"north",", spelled out rather\nthan ",[33,419,420],{},"[lat, lon]",". This is the most common early mistake, because the order\nis exactly reversed relative to GeoJSON. ",[33,423,424],{},"west > east"," is not an error but a\nbox crossing the antimeridian.",[18,427,429],{"id":428},"limits","Limits",[14,431,432,433,435],{},"The values that apply to you are in the ",[33,434,35],{}," frame. The rule: the first\nmistake is a message, a repeated one disconnects. A typo costs you the\nchange, not the connection.",[302,437,438,451],{},[305,439,440],{},[308,441,442,445,448],{},[311,443,444],{},"Limit",[311,446,447],{},"Value",[311,449,450],{},"When exceeded",[321,452,453,464],{},[308,454,455,458,461],{},[326,456,457],{},"Concurrent connections",[326,459,460],{},"2",[326,462,463],{},"Close 4005",[308,465,466,469,472],{},[326,467,468],{},"Subscriptions per connection",[326,470,471],{},"4",[326,473,474],{},"error",[14,476,477],{},"Close 4012 means we redeployed. Come back after 5 to 30 seconds of random\ndelay, not immediately — otherwise everyone returns at once.",[18,479,481],{"id":480},"next","Next",[259,483,484,492,499],{},[262,485,486,491],{},[487,488,490],"a",{"href":489},"\u002Fen\u002Fdocs\u002Fstream\u002Fprotocol","Protocol reference"," — Every frame, every field, all fourteen close codes.",[262,493,494,498],{},[487,495,497],{"href":496},"\u002Fen\u002Fdocs\u002Fstream\u002Fexamples","Example clients"," — Python and Go, with reconnection and loss detection.",[262,500,501,505],{},[487,502,504],{"href":503},"\u002Fen\u002Fdocs\u002Fstream\u002Faisstream","aisstream.io compatibility"," — Moving an existing aisstream.io client to Pelyr OPEN-AIS.",[507,508,509],"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":511},[512,513,514,515,516,517,518],{"id":20,"depth":78,"text":21},{"id":27,"depth":78,"text":28},{"id":52,"depth":78,"text":53},{"id":249,"depth":78,"text":250},{"id":296,"depth":78,"text":297},{"id":428,"depth":78,"text":429},{"id":480,"depth":78,"text":481},"Getting started with the Pelyr OPEN-AIS live AIS feed.","md",{},"\u002Fdocs\u002Fstream",{"title":5,"description":519},"docs\u002Fstream\u002Findex","EQB9jQoL1Q4oo2DOywBonBfekW4wgLuZMexQvDiwJ64",1789034411677]