[{"data":1,"prerenderedAt":823},["ShallowReactive",2],{"guide:authentication":3},{"id":4,"title":5,"body":6,"date":810,"description":811,"extension":812,"meta":813,"navigation":329,"path":814,"seo":815,"stem":816,"tags":817,"updated":821,"__hash__":822},"guides\u002Fdocs\u002Fauthentication.md","Authentication and API Tokens",{"type":7,"value":8,"toc":801},"minimark",[9,18,23,26,53,72,83,87,90,116,119,123,126,477,486,490,496,576,639,652,658,662,676,679,693,699,718,722,753,757,760,794,797],[10,11,12,13,17],"p",{},"Every call to the Wapito API is authenticated by one credential: the ",[14,15,16],"strong",{},"channel token",". There\nare no API keys at the account level, no OAuth dance and no per-request signing. You send the\ntoken as a Bearer header, and the token decides which WhatsApp number the request acts on. This\npage explains what that token is, what it can and cannot do, how to rotate it, and the three\nresponses a bad one gets.",[19,20,22],"h2",{"id":21},"what-a-channel-token-is","What a channel token is",[10,24,25],{},"A token is issued when a channel is created and looks like this:",[27,28,33],"pre",{"className":29,"code":30,"language":31,"meta":32,"style":32},"language-http shiki shiki-themes github-light-default github-dark-default","Authorization: Bearer wpt_9f2c4a7b1de35086c4b2e7a0f3d8c1b6a9e2f5d4\n","http","",[34,35,36],"code",{"__ignoreMap":32},[37,38,41,45,49],"span",{"class":39,"line":40},"line",1,[37,42,44],{"class":43},"sjgCt","Authorization",[37,46,48],{"class":47},"sjeE4",":",[37,50,52],{"class":51},"sSVrQ"," Bearer wpt_9f2c4a7b1de35086c4b2e7a0f3d8c1b6a9e2f5d4\n",[10,54,55,56,59,60,63,64,67,68,71],{},"The ",[34,57,58],{},"wpt_"," prefix is followed by exactly forty characters from ",[34,61,62],{},"A–Z",", ",[34,65,66],{},"a–z"," and ",[34,69,70],{},"0–9",", drawn\nfrom a cryptographic random source — about 238 bits of entropy, which is far more than anyone\nwill guess. The prefix exists so a secret scanner can recognise the string on sight; if one turns\nup in a commit, treat it as leaked and rotate it.",[10,73,74,75,78,79,82],{},"The token is shown ",[14,76,77],{},"once",", in the dashboard, at the moment the channel is created. Wapito\nstores only a SHA-256 hash of it, the way a password is stored. After that the dashboard can show\na masked preview (",[34,80,81],{},"wpt_9f2c…f5d4",") so you can tell two tokens apart, but nothing on our side can\ndisplay or e-mail the token itself. If it is gone, rotate.",[19,84,86],{"id":85},"the-scope-of-a-token","The scope of a token",[10,88,89],{},"A token belongs to exactly one channel, and a channel is exactly one linked WhatsApp number.\nThat has three practical consequences:",[91,92,93,104,110],"ul",{},[94,95,96,99,100,103],"li",{},[14,97,98],{},"No channel id in any path."," ",[34,101,102],{},"POST \u002Fmessages\u002Ftext"," sends from the number the token belongs\nto. If you run five numbers, you hold five tokens and pick the one for the number that should\nspeak.",[94,105,106,109],{},[14,107,108],{},"Everything the channel can do, the token can do."," There are no read-only tokens and no\nscopes in v1. Sending, reading chats, changing settings, creating webhooks and logging the\nphone out are all one credential. Guard it accordingly.",[94,111,112,115],{},[14,113,114],{},"A token cannot reach another channel",", even another one in the same account. A leaked token\nexposes one number, not your whole workspace.",[10,117,118],{},"The dashboard itself does not use channel tokens: it signs in with your account and talks to a\nseparate set of endpoints with a short-lived identity token. That is why creating a channel,\nissuing its token and rotating it are dashboard actions rather than API calls — the API cannot\nmint the credential that authorises it.",[19,120,122],{"id":121},"making-an-authenticated-request","Making an authenticated request",[10,124,125],{},"Send the header on every request. Check a token the same way the dashboard does, by asking for\nthe channel it belongs to:",[127,128,129,165,302],"code-group",{},[27,130,135],{"className":131,"code":132,"filename":133,"language":134,"meta":32,"style":32},"language-bash shiki shiki-themes github-light-default github-dark-default","curl https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fchannel \\\n  -H \"Authorization: Bearer $WAPITO_TOKEN\"\n","curl","bash",[34,136,137,148],{"__ignoreMap":32},[37,138,139,142,145],{"class":39,"line":40},[37,140,133],{"class":141},"sTDnQ",[37,143,144],{"class":51}," https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fchannel",[37,146,147],{"class":47}," \\\n",[37,149,151,155,158,162],{"class":39,"line":150},2,[37,152,154],{"class":153},"sHrmB","  -H",[37,156,157],{"class":51}," \"Authorization: Bearer ",[37,159,161],{"class":160},"s4rv2","$WAPITO_TOKEN",[37,163,164],{"class":51},"\"\n",[27,166,171],{"className":167,"code":168,"filename":169,"language":170,"meta":32,"style":32},"language-javascript shiki shiki-themes github-light-default github-dark-default","const res = await fetch('https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fchannel', {\n  headers: { Authorization: `Bearer ${process.env.WAPITO_TOKEN}` },\n});\nif (res.status === 401) throw new Error('token missing, malformed or revoked');\nconst channel = await res.json();\nconsole.log(channel.name, channel.status);\n","Node","javascript",[34,172,173,200,228,234,269,290],{"__ignoreMap":32},[37,174,175,178,181,184,187,191,194,197],{"class":39,"line":40},[37,176,177],{"class":47},"const",[37,179,180],{"class":153}," res",[37,182,183],{"class":47}," =",[37,185,186],{"class":47}," await",[37,188,190],{"class":189},"sbjLL"," fetch",[37,192,193],{"class":160},"(",[37,195,196],{"class":51},"'https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fchannel'",[37,198,199],{"class":160},", {\n",[37,201,202,205,208,211,214,217,219,222,225],{"class":39,"line":150},[37,203,204],{"class":160},"  headers: { Authorization: ",[37,206,207],{"class":51},"`Bearer ${",[37,209,210],{"class":160},"process",[37,212,213],{"class":51},".",[37,215,216],{"class":160},"env",[37,218,213],{"class":51},[37,220,221],{"class":153},"WAPITO_TOKEN",[37,223,224],{"class":51},"}`",[37,226,227],{"class":160}," },\n",[37,229,231],{"class":39,"line":230},3,[37,232,233],{"class":160},"});\n",[37,235,237,240,243,246,249,252,255,258,261,263,266],{"class":39,"line":236},4,[37,238,239],{"class":47},"if",[37,241,242],{"class":160}," (res.status ",[37,244,245],{"class":47},"===",[37,247,248],{"class":153}," 401",[37,250,251],{"class":160},") ",[37,253,254],{"class":47},"throw",[37,256,257],{"class":47}," new",[37,259,260],{"class":189}," Error",[37,262,193],{"class":160},[37,264,265],{"class":51},"'token missing, malformed or revoked'",[37,267,268],{"class":160},");\n",[37,270,272,274,277,279,281,284,287],{"class":39,"line":271},5,[37,273,177],{"class":47},[37,275,276],{"class":153}," channel",[37,278,183],{"class":47},[37,280,186],{"class":47},[37,282,283],{"class":160}," res.",[37,285,286],{"class":189},"json",[37,288,289],{"class":160},"();\n",[37,291,293,296,299],{"class":39,"line":292},6,[37,294,295],{"class":160},"console.",[37,297,298],{"class":189},"log",[37,300,301],{"class":160},"(channel.name, channel.status);\n",[27,303,308],{"className":304,"code":305,"filename":306,"language":307,"meta":32,"style":32},"language-python shiki shiki-themes github-light-default github-dark-default","import os\nimport requests\n\nres = requests.get(\n    \"https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fchannel\",\n    headers={\"Authorization\": f\"Bearer {os.environ['WAPITO_TOKEN']}\"},\n    timeout=(5, 30),\n)\nif res.status_code == 401:\n    raise SystemExit(res.json()[\"error\"][\"code\"])  # unauthorized or token_revoked\nprint(res.json()[\"status\"])\n","Python","python",[34,309,310,318,325,331,342,350,392,413,419,435,463],{"__ignoreMap":32},[37,311,312,315],{"class":39,"line":40},[37,313,314],{"class":47},"import",[37,316,317],{"class":160}," os\n",[37,319,320,322],{"class":39,"line":150},[37,321,314],{"class":47},[37,323,324],{"class":160}," requests\n",[37,326,327],{"class":39,"line":230},[37,328,330],{"emptyLinePlaceholder":329},true,"\n",[37,332,333,336,339],{"class":39,"line":236},[37,334,335],{"class":160},"res ",[37,337,338],{"class":47},"=",[37,340,341],{"class":160}," requests.get(\n",[37,343,344,347],{"class":39,"line":271},[37,345,346],{"class":51},"    \"https:\u002F\u002Fapi.wapito.com\u002Fv1\u002Fchannel\"",[37,348,349],{"class":160},",\n",[37,351,352,355,357,360,363,366,369,372,374,377,380,383,386,389],{"class":39,"line":292},[37,353,354],{"class":141},"    headers",[37,356,338],{"class":47},[37,358,359],{"class":160},"{",[37,361,362],{"class":51},"\"Authorization\"",[37,364,365],{"class":160},": ",[37,367,368],{"class":47},"f",[37,370,371],{"class":51},"\"Bearer ",[37,373,359],{"class":47},[37,375,376],{"class":160},"os.environ[",[37,378,379],{"class":51},"'WAPITO_TOKEN'",[37,381,382],{"class":160},"]",[37,384,385],{"class":47},"}",[37,387,388],{"class":51},"\"",[37,390,391],{"class":160},"},\n",[37,393,395,398,400,402,405,407,410],{"class":39,"line":394},7,[37,396,397],{"class":141},"    timeout",[37,399,338],{"class":47},[37,401,193],{"class":160},[37,403,404],{"class":153},"5",[37,406,63],{"class":160},[37,408,409],{"class":153},"30",[37,411,412],{"class":160},"),\n",[37,414,416],{"class":39,"line":415},8,[37,417,418],{"class":160},")\n",[37,420,422,424,427,430,432],{"class":39,"line":421},9,[37,423,239],{"class":47},[37,425,426],{"class":160}," res.status_code ",[37,428,429],{"class":47},"==",[37,431,248],{"class":153},[37,433,434],{"class":160},":\n",[37,436,438,441,444,447,450,453,456,459],{"class":39,"line":437},10,[37,439,440],{"class":47},"    raise",[37,442,443],{"class":153}," SystemExit",[37,445,446],{"class":160},"(res.json()[",[37,448,449],{"class":51},"\"error\"",[37,451,452],{"class":160},"][",[37,454,455],{"class":51},"\"code\"",[37,457,458],{"class":160},"])  ",[37,460,462],{"class":461},"sU953","# unauthorized or token_revoked\n",[37,464,466,469,471,474],{"class":39,"line":465},11,[37,467,468],{"class":153},"print",[37,470,446],{"class":160},[37,472,473],{"class":51},"\"status\"",[37,475,476],{"class":160},"])\n",[10,478,479,480,485],{},"Read the token from the environment or a secret manager, never from a constant in the source.\nThe generated code on every ",[481,482,484],"a",{"href":483},"\u002Fdocs\u002Fapi\u002F","API reference"," page follows the same rule, and so do the\nsnippets the dashboard writes for you.",[19,487,489],{"id":488},"what-a-bad-token-returns","What a bad token returns",[10,491,492,493,495],{},"Three codes, all in the standard error envelope. Branch on ",[34,494,34],{},", not on the message text.",[497,498,499,516],"table",{},[500,501,502],"thead",{},[503,504,505,509,513],"tr",{},[506,507,508],"th",{},"Status",[506,510,511],{},[34,512,34],{},[506,514,515],{},"Meaning",[517,518,519,539,553],"tbody",{},[503,520,521,527,532],{},[522,523,524],"td",{},[34,525,526],{},"401",[522,528,529],{},[34,530,531],{},"unauthorized",[522,533,534,535,538],{},"The header is missing, is not ",[34,536,537],{},"Bearer …",", or names a token Wapito has never issued.",[503,540,541,545,550],{},[522,542,543],{},[34,544,526],{},[522,546,547],{},[34,548,549],{},"token_revoked",[522,551,552],{},"The token was valid once and has been rotated since. The new token is in the dashboard.",[503,554,555,560,565],{},[522,556,557],{},[34,558,559],{},"403",[522,561,562],{},[34,563,564],{},"channel_locked",[522,566,567,568,571,572,575],{},"The token is fine but the channel is locked — ",[34,569,570],{},"details.reason"," says why, most often ",[34,573,574],{},"plan_required"," after a subscription lapsed.",[27,577,580],{"className":578,"code":579,"language":286,"meta":32,"style":32},"language-json shiki shiki-themes github-light-default github-dark-default","{\n  \"error\": {\n    \"code\": \"token_revoked\",\n    \"message\": \"This channel token has been revoked.\",\n    \"request_id\": \"req_01JRQ8F4X9N2K7YB3C5V6W8H0T\"\n  }\n}\n",[34,581,582,587,595,607,619,629,634],{"__ignoreMap":32},[37,583,584],{"class":39,"line":40},[37,585,586],{"class":160},"{\n",[37,588,589,592],{"class":39,"line":150},[37,590,591],{"class":43},"  \"error\"",[37,593,594],{"class":160},": {\n",[37,596,597,600,602,605],{"class":39,"line":230},[37,598,599],{"class":43},"    \"code\"",[37,601,365],{"class":160},[37,603,604],{"class":51},"\"token_revoked\"",[37,606,349],{"class":160},[37,608,609,612,614,617],{"class":39,"line":236},[37,610,611],{"class":43},"    \"message\"",[37,613,365],{"class":160},[37,615,616],{"class":51},"\"This channel token has been revoked.\"",[37,618,349],{"class":160},[37,620,621,624,626],{"class":39,"line":271},[37,622,623],{"class":43},"    \"request_id\"",[37,625,365],{"class":160},[37,627,628],{"class":51},"\"req_01JRQ8F4X9N2K7YB3C5V6W8H0T\"\n",[37,630,631],{"class":39,"line":292},[37,632,633],{"class":160},"  }\n",[37,635,636],{"class":39,"line":394},[37,637,638],{"class":160},"}\n",[10,640,55,641,644,645,647,648,651],{},[34,642,643],{},"request_id"," is in every error response. Quote it when you write to support: it is how we\nfind the exact request in our logs. A ",[34,646,526],{}," is never retried by a well-behaved client — nothing\nchanges between one attempt and the next — and a ",[34,649,650],{},"403 channel_locked"," is fixed in the dashboard\n(or by paying the invoice), not by code.",[10,653,654,655,657],{},"Authentication runs before rate limiting, and unauthenticated traffic is deliberately damped, so\na script that loops on a ",[34,656,526],{}," is throttled quickly. Fix the token instead.",[19,659,661],{"id":660},"rotating-a-token","Rotating a token",[10,663,664,665,668,669,672,673,213],{},"Open the channel's ",[14,666,667],{},"API"," tab and choose ",[14,670,671],{},"Rotate",". The dashboard asks you to confirm, then\nshows the new token once. The old token stops working immediately — there is no overlap window\nin which both are valid — and every request still carrying it gets ",[34,674,675],{},"401 token_revoked",[10,677,678],{},"Rotate when:",[91,680,681,684,687,690],{},[94,682,683],{},"a token appears anywhere it should not: a commit, a log line, a screenshot, a support ticket;",[94,685,686],{},"someone who had access to the secret store leaves;",[94,688,689],{},"you are handing a channel from one system to another and want the old one cut off;",[94,691,692],{},"you simply have not rotated in a while. There is no cost to it.",[10,694,695,696,698],{},"Because there is no overlap, plan a rotation as a two-step deploy: write the new token into your\nsecret store first, then rotate, then restart whatever reads the store. A service that reads the\ntoken once at start-up will see a handful of ",[34,697,675],{}," responses between the rotation\nand its restart; a service that reads it per request sees none.",[700,701,704],"callout",{"title":702,"type":703},"Rotating does not sign the phone out","warn",[10,705,706,707,710,711,717],{},"A rotation only replaces the credential. The WhatsApp session, the chats and the webhooks stay\nexactly as they were, and events keep flowing. If you also want the phone unlinked — a device you\nno longer control — use ",[14,708,709],{},"Log out"," on the Connect tab, or\n",[712,713],"endpoint-ref",{"label":714,"op":715,"tag":716},"POST \u002Fchannel\u002Flogout","logout-channel","channel"," while the old token\nis still valid.",[19,719,721],{"id":720},"keeping-the-token-secret","Keeping the token secret",[91,723,724,730,739,745],{},[94,725,726,729],{},[14,727,728],{},"Server-side only."," The API does not accept requests from browser origins other than the\ndashboard's, and a token in a front-end bundle is public the moment the page loads. Put a small\nbackend in between.",[94,731,732,735,736,738],{},[14,733,734],{},"Headers, never URLs."," Query strings end up in access logs, browser histories and referrer\nheaders. The API reads the token from the ",[34,737,44],{}," header and nowhere else.",[94,740,741,744],{},[14,742,743],{},"One token per environment."," Use a sandbox channel for development and a separate channel for\nproduction, so a leaked development token cannot send from the number customers know.",[94,746,747,752],{},[14,748,749,750,213],{},"Scan for ",[34,751,58],{}," Add the prefix to your secret scanner and pre-commit hooks.",[19,754,756],{"id":755},"the-other-two-credentials","The other two credentials",[10,758,759],{},"Two more secrets appear in the API, and neither authenticates a request:",[91,761,762,781],{},[94,763,764,765,768,769,772,773,776,777,213],{},"A ",[14,766,767],{},"webhook signing secret"," (",[34,770,771],{},"whsec_…",") is returned once when you create a webhook. Wapito uses\nit to sign every delivery, and your receiver uses it to verify the ",[34,774,775],{},"X-Wapito-Signature"," header.\nIt is for checking inbound traffic, not for calling the API — see ",[481,778,780],{"href":779},"\u002Fdocs\u002Fwebhooks\u002F","Webhooks",[94,782,764,783,768,786,789,790,213],{},[14,784,785],{},"signed media link",[34,787,788],{},"\u002Fmedia\u002F{id}?exp=…&sig=…",") is a time-limited URL for one stored file.\nIt carries its own credential, expires after 24 hours on Sandbox and 7 days on Premium, and is\nsafe to hand to a browser or a downstream service that must never see the channel token — see\n",[481,791,793],{"href":792},"\u002Fdocs\u002Fmedia\u002F","Media",[10,795,796],{},"Keep all three in the same secret store, and rotate the channel token first if you are ever\nunsure which one leaked.",[798,799,800],"style",{},"html pre.shiki code .sjgCt, html code.shiki .sjgCt{--shiki-default:#116329;--shiki-dark:#7EE787}html pre.shiki code .sjeE4, html code.shiki .sjeE4{--shiki-default:#CF222E;--shiki-dark:#FF7B72}html pre.shiki code .sSVrQ, html code.shiki .sSVrQ{--shiki-default:#0A3069;--shiki-dark:#A5D6FF}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);}html pre.shiki code .sTDnQ, html code.shiki .sTDnQ{--shiki-default:#953800;--shiki-dark:#FFA657}html pre.shiki code .sHrmB, html code.shiki .sHrmB{--shiki-default:#0550AE;--shiki-dark:#79C0FF}html pre.shiki code .s4rv2, html code.shiki .s4rv2{--shiki-default:#1F2328;--shiki-dark:#E6EDF3}html pre.shiki code .sbjLL, html code.shiki .sbjLL{--shiki-default:#8250DF;--shiki-dark:#D2A8FF}html pre.shiki code .sU953, html code.shiki .sU953{--shiki-default:#6E7781;--shiki-dark:#8B949E}",{"title":32,"searchDepth":150,"depth":150,"links":802},[803,804,805,806,807,808,809],{"id":21,"depth":150,"text":22},{"id":85,"depth":150,"text":86},{"id":121,"depth":150,"text":122},{"id":488,"depth":150,"text":489},{"id":660,"depth":150,"text":661},{"id":720,"depth":150,"text":721},{"id":755,"depth":150,"text":756},"2026-09-15","How Wapito Bearer tokens work: issuing and revoking them, the scope of a channel token, rotating a leaked key and the errors a bad token returns.","md",{},"\u002Fdocs\u002Fauthentication",{"title":5,"description":811},"docs\u002Fauthentication",[818,819,820],"authentication","tokens","security",null,"Bg7C04kZnuJlPTCo-GTackuN5gCM7J8VXgdcRBUAgnw",1790464894220]