[{"data":1,"prerenderedAt":25},["ShallowReactive",2],{"$ff6zmqf7cq2bh":3},{"href":4,"title":5,"description":6,"kind":7,"mark":7,"planned":8,"contributors":9,"provenance":7,"html":10,"headings":11},"\u002Fdocs\u002Fapi\u002Fauthentication","Authentication","API tokens: one credential per use, scoped to what it needs, and revocable on its own.",null,false,[],"\u003Ch2 id=\"api-tokens\">API tokens\u003C\u002Fh2>\n\u003Cp>Anything outside the portal (a script, a monitor, an agent) signs in with an \u003Cstrong>API token\u003C\u002Fstrong>. Create one under\n\u003Cstrong>Settings → API tokens\u003C\u002Fstrong>, and send it in the \u003Ccode>Authorization\u003C\u002Fcode> header:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Authorization: Bearer zhos_…\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>A token is only ever read from that header, never from a URL.\u003C\u002Fp>\n\u003Cp>The token is shown \u003Cstrong>once\u003C\u002Fstrong>, when you create or rotate it. Only a hash of it is stored, so nobody (Zero Human\nincluded) can read it back; the list shows its name, its last four characters, and when it was last used.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Rotate\u003C\u002Fstrong> issues a new secret with the same name and scopes, and the old one stops working at once. The replaced\ntoken stays listed, revoked, so the record outlives the credential.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Revoke\u003C\u002Fstrong> stops a token at once.\u003C\u002Fli>\n\u003Cli>A token acts for the enterprise it was created in.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"scopes\">Scopes\u003C\u002Fh2>\n\u003Cp>Every token has at least one scope, and there is no default: a credential's reach is always something someone\nchose. A scope is \u003Ccode>&lt;resource&gt;:&lt;level&gt;\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>the \u003Cstrong>resource\u003C\u002Fstrong> is the first part of a route's path after \u003Ccode>\u002Fv1\u003C\u002Fcode>, so \u003Ccode>runs:read\u003C\u002Fcode> reads \u003Ccode>\u002Fv1\u002Fruns\u003C\u002Fcode>,\n\u003Ccode>\u002Fv1\u002Fruns\u002F{id}\u003C\u002Fcode> and \u003Ccode>\u002Fv1\u002Fruns\u002F{id}\u002Flog\u003C\u002Fcode>;\u003C\u002Fli>\n\u003Cli>the \u003Cstrong>level\u003C\u002Fstrong> is \u003Ccode>read\u003C\u002Fcode> for \u003Ccode>GET\u003C\u002Fcode>, and \u003Ccode>write\u003C\u002Fcode> for everything else. \u003Ccode>write\u003C\u002Fcode> includes \u003Ccode>read\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The resources are \u003Ccode>blockers\u003C\u002Fcode>, \u003Ccode>catalog\u003C\u002Fcode>, \u003Ccode>chats\u003C\u002Fcode>, \u003Ccode>enterprise\u003C\u002Fcode>, \u003Ccode>executions\u003C\u002Fcode>, \u003Ccode>gates\u003C\u002Fcode>, \u003Ccode>llm\u003C\u002Fcode>, \u003Ccode>members\u003C\u002Fcode>, \u003Ccode>memory\u003C\u002Fcode>,\n\u003Ccode>orgs\u003C\u002Fcode>, \u003Ccode>plans\u003C\u002Fcode>, \u003Ccode>proposals\u003C\u002Fcode>, \u003Ccode>recommendations\u003C\u002Fcode>, \u003Ccode>roles\u003C\u002Fcode>, \u003Ccode>runs\u003C\u002Fcode>, \u003Ccode>spend\u003C\u002Fcode>, \u003Ccode>tasks\u003C\u002Fcode>, \u003Ccode>teams\u003C\u002Fcode>, \u003Ccode>tools\u003C\u002Fcode> and \u003Ccode>webhooks\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Because the resource is the first part of the path, a route nested under a task is a task route:\n\u003Ccode>POST \u002Fv1\u002Ftasks\u002F{id}\u002Fruns\u003C\u002Fcode> (start a run) needs \u003Ccode>tasks:write\u003C\u002Fcode>, not \u003Ccode>runs:write\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>A route whose first part is not a resource is refused to every token. New parts of the API are closed to tokens\nuntil they are given a resource, so a token never gains reach it was not granted.\u003C\u002Fp>\n\u003Cp>You can only grant a token scopes you hold yourself.\u003C\u002Fp>\n\u003Ch2 id=\"two-things-a-scope-does-not-tell-you\">Two things a scope does not tell you\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>\u003Ccode>gates:write\u003C\u002Fcode> really does decide gates\u003C\u002Fstrong>, including approving something going live. Grant it deliberately.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>No token can manage tokens\u003C\u002Fstrong>, whatever \u003Ccode>enterprise:write\u003C\u002Fcode> would otherwise reach. A token that could create\ntokens would make revoking one pointless: a leaked one would simply leave a fresh one behind.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"when-a-token-is-refused\">When a token is refused\u003C\u002Fh2>\n\u003Cdiv class=\"prose__table\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Status\u003C\u002Fth>\n\u003Cth>\u003Ccode>error\u003C\u002Fcode>\u003C\u002Fth>\n\u003Cth>What it means\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>401\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003Ctd>No token, or not one that is valid (unknown, revoked, or rotated away).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>403\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>api_token_scope\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The token lacks the scope this route needs. The message names it: &quot;This token needs runs:write; it has runs:read.&quot;\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>403\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>api_token_cannot_manage_tokens\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Token management is only in the portal.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n",[12,16,19,22],{"id":13,"text":14,"level":15,"planned":8},"api-tokens","API tokens",2,{"id":17,"text":18,"level":15,"planned":8},"scopes","Scopes",{"id":20,"text":21,"level":15,"planned":8},"two-things-a-scope-does-not-tell-you","Two things a scope does not tell you",{"id":23,"text":24,"level":15,"planned":8},"when-a-token-is-refused","When a token is refused",1791124519328]