{
  "page": "docs/authentication",
  "title": "Authentication and keys",
  "slug": "authentication",
  "description": "Agent keys, org keys and sessions: what each can do, how to send them, and how to rotate or revoke them.",
  "section": "Getting started",
  "updated": "2026-09-29",
  "blocks": [
    {
      "type": "p",
      "text": "Every request outside the public data carries one credential in the `Authorization` header. There are three kinds. An **agent key** lets your agent ask for a recommendation, buy a result and read its receipts. An **org key** lets a script or an agent run an org the way an owner does in the dashboard. A **session** is a signed-in person, or a script acting as one. This page says what each can do, how to send it, and how to rotate or revoke it."
    },
    {
      "type": "note",
      "text": "Arettic is pre-launch. Keys go to design partners; everyone else can [join the waitlist](/waitlist). The `@arettic/sdk` (npm), `arettic` (PyPI) and `@arettic/mcp` packages used in the samples are published at launch."
    },
    {
      "type": "h2",
      "id": "credentials",
      "text": "The three credentials"
    },
    {
      "type": "table",
      "caption": "The three credentials, side by side",
      "head": [
        "Starts with",
        "Credential",
        "Held by",
        "Reaches",
        "Made"
      ],
      "rows": [
        [
          "sk_test_… or sk_live_…",
          "Agent key",
          "One agent",
          "The buying endpoints: recommend, execute, jobs, receipts, disputes, approvals, balance, `GET /v1/agent` and the MCP server",
          "On the Agents page, or with `POST /v1/orgs/{orgId}/agents`"
        ],
        [
          "ok_…",
          "Org key",
          "A script or agent that runs the org",
          "Only `/v1/orgs/{orgId}/…` for its own org. A `read` key only reads. Never key management or a data-deletion request",
          "On the Team page by a signed-in owner, or with `POST /v1/orgs/{orgId}/keys`"
        ],
        [
          "ss_… or the arettic_session cookie",
          "Session",
          "A signed-in person, or a script acting as one",
          "Every account and org endpoint: `/v1/me`, `POST /v1/orgs`, `/v1/orgs/{orgId}/…`, `POST /auth/logout`",
          "`POST /auth/email/start`, then the emailed link; or Google sign-in"
        ]
      ]
    },
    {
      "type": "p",
      "text": "Public data needs no credential: `GET /v1/tools`, `/v1/scores`, `/v1/task-types`, `/v1/pricing`, `/v1/formula`, `/v1/reports` and `/v1/status`. See [public data](/docs/public-data)."
    },
    {
      "type": "h2",
      "id": "authorization-header",
      "text": "Send the header"
    },
    {
      "type": "p",
      "text": "Send the credential as a bearer token. The header is the same for all three kinds."
    },
    {
      "type": "code",
      "title": "Header",
      "lang": "http",
      "code": "Authorization: Bearer sk_test_…"
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl https://api.arettic.com/v1/agent \\\n  -H \"Authorization: Bearer sk_test_…\""
    },
    {
      "type": "p",
      "text": "Browsers use the `arettic_session` cookie instead. The API sets it when you open a sign-in link and reads it when the `Authorization` header carries no session token or org key. Scripts and agents send the header and can ignore the cookie."
    },
    {
      "type": "p",
      "text": "The SDKs and the MCP bridge read the key from the `ARETTIC_API_KEY` environment variable, and the API address from `ARETTIC_API_URL` (default `https://api.arettic.com`). A key never has to sit in code."
    },
    {
      "type": "h2",
      "id": "agent-keys",
      "text": "Agent keys"
    },
    {
      "type": "p",
      "text": "Each agent has one key, and the key names the agent. The agent carries the mode, the monthly budget, the approval threshold and the IP allowlist, so the key carries them too. A test agent's key starts with `sk_test_`: it buys from mock tools and no credits move. A live agent's key starts with `sk_live_`: it buys real results with the org's credits and writes receipts. The mode is fixed when the agent is made; for the other mode, make another agent. [Test mode](/docs/test-mode) says what a test key returns."
    },
    {
      "type": "h3",
      "id": "get-an-agent-key",
      "text": "Get a key"
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "Sign in and open [Agents](/app/agents). Add an agent: a name (up to 80 characters), test or live, a monthly budget in dollars, and when to ask an owner before a single purchase.",
        "The key appears once, on that page, for 2 minutes. Copy it now. We keep only a SHA-256 hash, so we can't show it again.",
        "Give it to your agent as `ARETTIC_API_KEY`."
      ]
    },
    {
      "type": "p",
      "text": "A script can do the same with a session token or a write org key. Any member of the org can add an agent. Budgets and thresholds in the API are whole credits (1 credit = $0.001): `monthly_budget_credits` defaults to 50000 ($50 a calendar month, UTC) and `approval_threshold_credits` to 20000 ($20), with `null` meaning never ask. `mode` defaults to `test`. At the plan's limit on active agents the API answers 402 `plan_limit`."
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl -X POST https://api.arettic.com/v1/orgs/$ORG_ID/agents \\\n  -H \"Authorization: Bearer ok_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"Research agent\", \"mode\": \"test\"}'"
    },
    {
      "type": "code",
      "title": "TypeScript",
      "lang": "ts",
      "code": "import { AretticOrg } from \"@arettic/sdk\";\n\nconst org = new AretticOrg({\n  apiKey: process.env.ARETTIC_ORG_KEY, // an ok_… key\n  orgId: process.env.ARETTIC_ORG_ID!,\n});\nconst { agent, key } = await org.agents.create({ name: \"Research agent\", mode: \"test\" });\nconsole.log(agent.id, key); // the key is in this answer only"
    },
    {
      "type": "code",
      "title": "Python",
      "lang": "python",
      "code": "import os\nfrom arettic import AretticOrg\n\norg = AretticOrg(api_key=os.environ[\"ARETTIC_ORG_KEY\"], org_id=os.environ[\"ARETTIC_ORG_ID\"])\nmade = org.agents.create(name=\"Research agent\", mode=\"test\")\nprint(made[\"agent\"][\"id\"], made[\"key\"])  # the key is in this answer only"
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{\n  \"agent\": {\n    \"id\": \"2f6c1c0e-5b1a-4d8e-9a3b-7c1d2e3f4a5b\",\n    \"name\": \"Research agent\",\n    \"mode\": \"test\",\n    \"status\": \"active\",\n    \"monthly_budget\": { \"credits\": \"50000\", \"usd\": \"50.000\" },\n    \"approval_threshold\": { \"credits\": \"20000\", \"usd\": \"20.000\" },\n    \"ip_allowlist\": [],\n    \"key_prefix\": \"sk_test_Ab12\",\n    \"key_last_used_at\": null,\n    \"created_at\": \"2026-09-28T14:02:11.000Z\"\n  },\n  \"key\": \"sk_test_…\",\n  \"key_note\": \"Shown once. Store it now; we keep only a hash.\"\n}"
    },
    {
      "type": "h3",
      "id": "agent-key-endpoints",
      "text": "What an agent key reaches"
    },
    {
      "type": "table",
      "caption": "Endpoints that take an agent key",
      "head": [
        "Endpoint",
        "What it does"
      ],
      "rows": [
        [
          "POST /v1/recommend",
          "Ranked tools for a task. See [recommend](/docs/recommend)."
        ],
        [
          "POST /v1/execute",
          "Run a tool; charged only if the result passes its check. See [execute](/docs/execute)."
        ],
        [
          "GET /v1/jobs/{id}",
          "A batch of more than 25 items: status, progress and per-item outcomes. See [jobs](/docs/jobs)."
        ],
        [
          "GET /v1/receipts",
          "The org's receipts, newest first. See [receipts](/docs/receipts)."
        ],
        [
          "GET /v1/receipts/{id}",
          "One receipt: each item's provider, cost, result hash, check version, outcome and refund."
        ],
        [
          "POST /v1/disputes",
          "Dispute a charged item within 7 days. See [disputes](/docs/disputes)."
        ],
        [
          "GET /v1/disputes/{id}",
          "A dispute's status, decision and refund."
        ],
        [
          "GET /v1/approvals/{id}",
          "An approval request's status, locked tool and amount, and expiry. See [budgets and approvals](/docs/budgets-and-approvals)."
        ],
        [
          "GET /v1/balance",
          "The agent's month-to-date spend, its budget and the org's credits."
        ],
        [
          "GET /v1/agent",
          "The calling agent and its limits."
        ],
        [
          "POST /mcp",
          "The MCP server over Streamable HTTP. See [MCP](/docs/mcp)."
        ]
      ]
    },
    {
      "type": "p",
      "text": "An agent key can't call the account or org endpoints (`/v1/me`, `/v1/orgs/{orgId}/…`). They answer 401 `unauthenticated`. Use an org key or a session there."
    },
    {
      "type": "h3",
      "id": "check-a-key",
      "text": "Check a key"
    },
    {
      "type": "p",
      "text": "`GET /v1/agent` answers with the agent the key belongs to and its limits. It is the quickest way to confirm that a key works, and to see which agent, mode and budget a key carries."
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl https://api.arettic.com/v1/agent \\\n  -H \"Authorization: Bearer $ARETTIC_API_KEY\""
    },
    {
      "type": "code",
      "title": "TypeScript",
      "lang": "ts",
      "code": "import { Arettic } from \"@arettic/sdk\";\n\nconst arettic = new Arettic({ apiKey: process.env.ARETTIC_API_KEY });\nconst agent = await arettic.me();\nconsole.log(agent.name, agent.mode, agent.monthly_budget.usd);"
    },
    {
      "type": "code",
      "title": "Python",
      "lang": "python",
      "code": "from arettic import Arettic\n\nclient = Arettic()  # reads ARETTIC_API_KEY\nagent = client.me()[\"agent\"]\nprint(agent[\"name\"], agent[\"mode\"], agent[\"monthly_budget\"][\"usd\"])"
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{\n  \"agent\": {\n    \"id\": \"2f6c1c0e-5b1a-4d8e-9a3b-7c1d2e3f4a5b\",\n    \"name\": \"Research agent\",\n    \"mode\": \"test\",\n    \"status\": \"active\",\n    \"monthly_budget\": { \"credits\": \"50000\", \"usd\": \"50.000\" },\n    \"approval_threshold\": { \"credits\": \"20000\", \"usd\": \"20.000\" },\n    \"ip_allowlist\": [],\n    \"key_prefix\": \"sk_test_Ab12\",\n    \"key_last_used_at\": \"2026-09-29T09:41:00.000Z\",\n    \"created_at\": \"2026-09-28T14:02:11.000Z\"\n  }\n}"
    },
    {
      "type": "h3",
      "id": "agent-keys-mcp",
      "text": "Use the key with MCP"
    },
    {
      "type": "p",
      "text": "The MCP server takes the same key. A client that speaks Streamable HTTP connects to `https://api.arettic.com/mcp` with `Authorization: Bearer sk_…`. Other clients run the `@arettic/mcp` bridge, which reads the key from `ARETTIC_API_KEY` and never prints it. Org keys don't work here. See [MCP](/docs/mcp) for the tools and the client setups."
    },
    {
      "type": "code",
      "title": "MCP config",
      "lang": "json",
      "code": "{\n  \"mcpServers\": {\n    \"arettic\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@arettic/mcp\"],\n      \"env\": { \"ARETTIC_API_KEY\": \"sk_test_…\" }\n    }\n  }\n}"
    },
    {
      "type": "h3",
      "id": "agent-rate-limit",
      "text": "Rate limit"
    },
    {
      "type": "p",
      "text": "An agent key can make 600 requests a minute, counted across every endpoint above and `/mcp`. Every answer to a request with a valid key carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window resets). Over the limit the API answers 429 `rate_limited` with a `Retry-After` header. The SDKs wait and retry reads by themselves. The other limits are on [rate limits](/docs/rate-limits)."
    },
    {
      "type": "h2",
      "id": "org-keys",
      "text": "Org keys"
    },
    {
      "type": "p",
      "text": "An org key lets a script or an agent do what a person does in the dashboard, through the API: add agents, rotate their keys, read receipts, decide approvals, add webhooks and more. The full list of endpoints is on [org API](/docs/org-api). This section covers what the key is and what it can't do."
    },
    {
      "type": "list",
      "items": [
        "It starts with `ok_` and has a scope. A `read` key can only send `GET` requests. A `write` key can do anything its maker can.",
        "It acts as the person who made it, with that person's current role. Every role check that applies to them applies to the key.",
        "It works only on its own org's endpoints, `/v1/orgs/{orgId}/…`. Anywhere else it answers 403 `forbidden`, including the agent endpoints and `/v1/me`.",
        "It can't manage keys. `/v1/orgs/{orgId}/keys` needs a signed-in owner, so a leaked key can't make itself a successor.",
        "It can read data-deletion requests but not make one. `POST /v1/orgs/{orgId}/deletion-requests` needs a signed-in owner.",
        "It stops working the moment its maker leaves the org or is removed, and when an owner revokes it.",
        "An org can have up to 20 keys. Each key can make 300 requests a minute, with the same `RateLimit-*` headers as agent keys."
      ]
    },
    {
      "type": "h3",
      "id": "make-an-org-key",
      "text": "Make an org key"
    },
    {
      "type": "p",
      "text": "Only an owner can make or revoke org keys, and only while signed in: on [Team](/app/team) under Org API keys (a name, then read or write), or with a session token. A member's session answers 403 `forbidden`."
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl -X POST https://api.arettic.com/v1/orgs/$ORG_ID/keys \\\n  -H \"Authorization: Bearer ss_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"Billing agent\", \"scope\": \"read\"}'"
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{\n  \"key\": {\n    \"id\": \"5b7d9f1a-2c3e-4d5f-8a9b-0c1d2e3f4a5b\",\n    \"name\": \"Billing agent\",\n    \"scope\": \"read\",\n    \"prefix\": \"ok_Ab12Cd3\",\n    \"created_by_email\": \"emily@example.com\",\n    \"created_at\": \"2026-09-29T10:20:00.000Z\",\n    \"last_used_at\": null\n  },\n  \"secret\": \"ok_…\",\n  \"note\": \"Shown once. Store it now; we keep only a hash.\"\n}"
    },
    {
      "type": "p",
      "text": "Any member can list the org's keys with `GET /v1/orgs/{orgId}/keys` while signed in. The list shows each key's name, scope, prefix, maker and last use, never the key itself."
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{\n  \"keys\": [\n    {\n      \"id\": \"5b7d9f1a-2c3e-4d5f-8a9b-0c1d2e3f4a5b\",\n      \"name\": \"Billing agent\",\n      \"scope\": \"read\",\n      \"prefix\": \"ok_Ab12Cd3\",\n      \"created_by_email\": \"emily@example.com\",\n      \"created_at\": \"2026-09-29T10:20:00.000Z\",\n      \"last_used_at\": \"2026-09-29T11:02:00.000Z\"\n    }\n  ]\n}"
    },
    {
      "type": "h3",
      "id": "use-an-org-key",
      "text": "Use an org key"
    },
    {
      "type": "p",
      "text": "Send it as a bearer token on any `/v1/orgs/{orgId}/…` endpoint of its org. The `AretticOrg` clients take the key and the org id."
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl https://api.arettic.com/v1/orgs/$ORG_ID/agents \\\n  -H \"Authorization: Bearer ok_…\""
    },
    {
      "type": "code",
      "title": "TypeScript",
      "lang": "ts",
      "code": "import { AretticOrg } from \"@arettic/sdk\";\n\nconst org = new AretticOrg({\n  apiKey: process.env.ARETTIC_ORG_KEY,\n  orgId: process.env.ARETTIC_ORG_ID!,\n});\nconst agents = await org.agents.list();\nconsole.log(agents.map((a) => [a.name, a.mode, a.key_prefix]));"
    },
    {
      "type": "code",
      "title": "Python",
      "lang": "python",
      "code": "import os\nfrom arettic import AretticOrg\n\norg = AretticOrg(api_key=os.environ[\"ARETTIC_ORG_KEY\"], org_id=os.environ[\"ARETTIC_ORG_ID\"])\nfor a in org.agents.list()[\"agents\"]:\n    print(a[\"name\"], a[\"mode\"], a[\"key_prefix\"])"
    },
    {
      "type": "p",
      "text": "With a `read` key, every method that writes answers 403 `forbidden`. The `AretticOrg` clients leave out key management on purpose: making and revoking keys needs a person."
    },
    {
      "type": "h2",
      "id": "sessions",
      "text": "Sessions"
    },
    {
      "type": "p",
      "text": "A session is how a person uses the dashboard, and how a script acts as a person. Some things need one: creating an org, making or revoking org keys, and asking for data deletion. Sign-in is by email link. Google sign-in works where it is configured. Customer accounts have no passwords."
    },
    {
      "type": "h3",
      "id": "email-sign-in",
      "text": "Sign in by email"
    },
    {
      "type": "list",
      "ordered": true,
      "items": [
        "`POST /auth/email/start` with the address. The API answers 202 `{\"status\": \"sent\"}` and emails a one-time link. A new address becomes an account when its link is first used: the link is the proof that the email is real.",
        "The link points at `GET /auth/email/verify?token=…`. It works once and expires after 15 minutes. Opened in a browser, it sets the `arettic_session` cookie and sends you to the dashboard.",
        "For a script, take the `token` from the link and send it to `POST /auth/email/verify`. The answer holds `session_token` (`ss_…`), `user_id` and `new_user`. Send the token as a bearer token from then on.",
        "A session lasts 30 days. `POST /auth/logout` ends it early."
      ]
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl -X POST https://api.arettic.com/auth/email/start \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"emily@example.com\"}'"
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{ \"status\": \"sent\" }"
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "# The token is the token= part of the link in the email.\ncurl -X POST https://api.arettic.com/auth/email/verify \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"token\": \"…\"}'"
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{\n  \"session_token\": \"ss_…\",\n  \"user_id\": \"0d9e8f7a-6b5c-4d3e-9f1a-0b9c8d7e6f5a\",\n  \"new_user\": false\n}"
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl https://api.arettic.com/v1/me \\\n  -H \"Authorization: Bearer ss_…\""
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{\n  \"user\": {\n    \"id\": \"0d9e8f7a-6b5c-4d3e-9f1a-0b9c8d7e6f5a\",\n    \"email\": \"emily@example.com\",\n    \"name\": null,\n    \"email_verified\": true,\n    \"phone\": \"+14155550132\",\n    \"phone_verified\": true\n  },\n  \"orgs\": [\n    { \"id\": \"7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d\", \"name\": \"Acme\", \"role\": \"owner\" }\n  ]\n}"
    },
    {
      "type": "code",
      "title": "TypeScript",
      "lang": "ts",
      "code": "// No SDK for sessions: plain fetch.\nconst api = \"https://api.arettic.com\";\n\nawait fetch(api + \"/auth/email/start\", {\n  method: \"POST\",\n  headers: { \"content-type\": \"application/json\" },\n  body: JSON.stringify({ email: \"emily@example.com\" }),\n});\n\n// Take the token from the link in the email, then:\nconst verified = await fetch(api + \"/auth/email/verify\", {\n  method: \"POST\",\n  headers: { \"content-type\": \"application/json\" },\n  body: JSON.stringify({ token: process.env.ARETTIC_LOGIN_TOKEN }),\n});\nconst { session_token } = await verified.json();\n\nconst me = await fetch(api + \"/v1/me\", {\n  headers: { authorization: \"Bearer \" + session_token },\n});\nconsole.log(await me.json());"
    },
    {
      "type": "code",
      "title": "Python",
      "lang": "python",
      "code": "# No SDK for sessions: urllib from the standard library.\nimport json\nimport os\nimport urllib.request\n\nAPI = \"https://api.arettic.com\"\n\n\ndef call(method, path, body=None, token=None):\n    data = None if body is None else json.dumps(body).encode()\n    headers = {\"Content-Type\": \"application/json\"}\n    if token:\n        headers[\"Authorization\"] = \"Bearer \" + token\n    req = urllib.request.Request(API + path, data=data, method=method, headers=headers)\n    with urllib.request.urlopen(req) as res:\n        return json.load(res)\n\n\ncall(\"POST\", \"/auth/email/start\", {\"email\": \"emily@example.com\"})\n# Take the token from the link in the email, then:\nsession = call(\"POST\", \"/auth/email/verify\", {\"token\": os.environ[\"ARETTIC_LOGIN_TOKEN\"]})\nme = call(\"GET\", \"/v1/me\", token=session[\"session_token\"])\nprint(me[\"user\"][\"email\"], [o[\"name\"] for o in me[\"orgs\"]])"
    },
    {
      "type": "p",
      "text": "Sign-in has two limits: 20 starts an hour per IP address, and 5 emails an hour per address. Over either, the API answers 429 `rate_limited`. A used, expired or unknown token answers 401 `invalid_token`; ask for a new link."
    },
    {
      "type": "h3",
      "id": "google-sign-in",
      "text": "Google sign-in"
    },
    {
      "type": "p",
      "text": "Open `GET /auth/google/start` in a browser. It sends you to Google to pick an account, then back to `GET /auth/google/callback`, which sets the cookie and sends you to the dashboard. Accounts are matched by email, so an email-link account and a Google account with the same address are one account. Google sign-in is for browsers only; there is no token variant."
    },
    {
      "type": "list",
      "items": [
        "Google must have verified the account's email, or the sign-in answers 401 `unverified_email`. Use an email link instead.",
        "The sign-in has to finish within 10 minutes, in the browser that started it, or it answers 400 `invalid_state`. Start again.",
        "If Google doesn't complete the sign-in, the answer is 401 `google_failed`. Try again, or use an email link.",
        "Where Google sign-in isn't switched on, `/auth/google/start` answers 503 `not_configured`. Use an email link."
      ]
    },
    {
      "type": "h3",
      "id": "session-cookie",
      "text": "The session cookie"
    },
    {
      "type": "p",
      "text": "The cookie is called `arettic_session`. It is `HttpOnly`, so page scripts can't read it; `SameSite=Lax`, so it travels with the redirect from the sign-in link; `Secure` in production; and it lasts 30 days. The API reads it only when the `Authorization` header carries no session token or org key. `POST /auth/logout` clears it."
    },
    {
      "type": "h2",
      "id": "rotate-and-revoke",
      "text": "Rotate and revoke"
    },
    {
      "type": "h3",
      "id": "rotate-agent-key",
      "text": "Agent keys"
    },
    {
      "type": "p",
      "text": "Rotating issues a new key and revokes the old one in the same step. The old key is refused from that moment, so give the agent the new key straight away. Revoking stops the key without issuing a new one; the agent can't buy until you issue another. Any member of the org can do either."
    },
    {
      "type": "list",
      "items": [
        "Dashboard: [Agents](/app/agents), open **Key for** the agent, tick the confirmation, then **Rotate key** or **Revoke key**. The new key shows once, for 2 minutes. An agent with no key shows **Issue a key** instead.",
        "API: `POST /v1/orgs/{orgId}/agents/{agentId}/rotate-key` and `POST /v1/orgs/{orgId}/agents/{agentId}/revoke-key`, with a session token or a write org key. `rotate-key` also issues a key for an agent that has none."
      ]
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl -X POST https://api.arettic.com/v1/orgs/$ORG_ID/agents/$AGENT_ID/rotate-key \\\n  -H \"Authorization: Bearer ok_…\"\n\ncurl -X POST https://api.arettic.com/v1/orgs/$ORG_ID/agents/$AGENT_ID/revoke-key \\\n  -H \"Authorization: Bearer ok_…\""
    },
    {
      "type": "code",
      "title": "TypeScript",
      "lang": "ts",
      "code": "const { key } = await org.agents.rotateKey(agentId); // the old key stopped working\nawait org.agents.revokeKey(agentId); // { status: \"revoked\" }"
    },
    {
      "type": "code",
      "title": "Python",
      "lang": "python",
      "code": "key = org.agents.rotate_key(agent_id)[\"key\"]  # the old key stopped working\norg.agents.revoke_key(agent_id)  # {\"status\": \"revoked\"}"
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{ \"key\": \"sk_live_…\", \"key_note\": \"The previous key stopped working immediately.\" }"
    },
    {
      "type": "p",
      "text": "To pause an agent without touching its key, set its `status` to `disabled`: `PATCH /v1/orgs/{orgId}/agents/{agentId}` with `{\"status\": \"disabled\"}`, or **Edit** on the Agents page. Its key is refused with 401 `unauthenticated` until the agent is active again. If you think the key leaked, rotate it as well."
    },
    {
      "type": "h3",
      "id": "revoke-org-key",
      "text": "Org keys"
    },
    {
      "type": "p",
      "text": "Org keys don't rotate. Make a new key, move the caller to it, then revoke the old one. Revoking needs a signed-in owner: **Revoke** on the Team page, or `DELETE /v1/orgs/{orgId}/keys/{id}` with a session token. A revoked key answers 401 `unauthenticated` from then on; a key that is already revoked or unknown answers 404 `not_found`."
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl -X DELETE https://api.arettic.com/v1/orgs/$ORG_ID/keys/$KEY_ID \\\n  -H \"Authorization: Bearer ss_…\""
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{ \"status\": \"revoked\" }"
    },
    {
      "type": "h3",
      "id": "sign-out",
      "text": "Sessions"
    },
    {
      "type": "p",
      "text": "`POST /auth/logout` with the session's token or cookie revokes it and clears the cookie. The answer is `{\"status\": \"signed_out\"}`. Sessions also end on their own after 30 days."
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl -X POST https://api.arettic.com/auth/logout \\\n  -H \"Authorization: Bearer ss_…\""
    },
    {
      "type": "h2",
      "id": "ip-allowlist",
      "text": "IP allowlist"
    },
    {
      "type": "p",
      "text": "Each agent can carry an allowlist of IP addresses and CIDR ranges, IPv4 or IPv6. Empty, the default, means any address. With entries, a request with that agent's key from any other address is refused with 403 `ip_not_allowed`. The check runs as soon as the key is found, before the endpoint does anything, and it covers `/mcp` too."
    },
    {
      "type": "p",
      "text": "The address checked is the one the request arrives from. Behind NAT, a VPN or a cloud egress that is the shared public address, not the machine's own. Put the public address your agent calls from on the list, and use a range when it runs from a pool of addresses."
    },
    {
      "type": "list",
      "items": [
        "Dashboard: [Agents](/app/agents), **Edit** the agent, then the **IP allowlist** field, one address or range per line. Leave it empty to allow any address.",
        "API: `PATCH /v1/orgs/{orgId}/agents/{agentId}` with `ip_allowlist`, with a session token or a write org key. Send `[]` to clear it. An entry that isn't an address or a range answers 400 `invalid_input`."
      ]
    },
    {
      "type": "code",
      "title": "curl",
      "lang": "bash",
      "code": "curl -X PATCH https://api.arettic.com/v1/orgs/$ORG_ID/agents/$AGENT_ID \\\n  -H \"Authorization: Bearer ok_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"ip_allowlist\": [\"203.0.113.7\", \"203.0.113.0/24\"]}'"
    },
    {
      "type": "code",
      "title": "TypeScript",
      "lang": "ts",
      "code": "const agent = await org.agents.update(agentId, {\n  ip_allowlist: [\"203.0.113.7\", \"203.0.113.0/24\"],\n});\nconsole.log(agent.ip_allowlist);"
    },
    {
      "type": "code",
      "title": "Python",
      "lang": "python",
      "code": "agent = org.agents.update(agent_id, ip_allowlist=[\"203.0.113.7\", \"203.0.113.0/24\"])\nprint(agent[\"agent\"][\"ip_allowlist\"])"
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{\n  \"agent\": {\n    \"id\": \"2f6c1c0e-5b1a-4d8e-9a3b-7c1d2e3f4a5b\",\n    \"name\": \"Research agent\",\n    \"mode\": \"live\",\n    \"status\": \"active\",\n    \"monthly_budget\": { \"credits\": \"50000\", \"usd\": \"50.000\" },\n    \"approval_threshold\": { \"credits\": \"20000\", \"usd\": \"20.000\" },\n    \"ip_allowlist\": [\"203.0.113.7\", \"203.0.113.0/24\"],\n    \"key_prefix\": \"sk_live_Ab12\",\n    \"key_last_used_at\": \"2026-09-29T09:41:00.000Z\",\n    \"created_at\": \"2026-09-28T14:02:11.000Z\"\n  }\n}"
    },
    {
      "type": "code",
      "title": "Response from another address (example)",
      "lang": "json",
      "code": "{\n  \"error\": {\n    \"code\": \"ip_not_allowed\",\n    \"message\": \"This key can't be used from this IP address\",\n    \"doc_url\": \"https://arettic.com/docs/errors#ip_not_allowed\",\n    \"retryable\": false\n  }\n}"
    },
    {
      "type": "p",
      "text": "Org keys and sessions have no allowlist."
    },
    {
      "type": "h2",
      "id": "storage",
      "text": "How keys are stored"
    },
    {
      "type": "list",
      "items": [
        "We store a SHA-256 hash of every key and session token, never the value. A key is shown once, when it is made or rotated. If you lose it, rotate it.",
        "We keep the first characters of each key so you can tell keys apart: `key_prefix` on an agent (12 characters) and `prefix` on an org key (10 characters). They appear in the dashboard and in `GET /v1/agent`.",
        "We record when a key was last used (`key_last_used_at` on an agent, `last_used_at` on an org key; updated at most once a minute) and, for agent keys, the addresses it has been used from, so a key that turns up from a new place can be looked into.",
        "A session records the address and user agent it was started from."
      ]
    },
    {
      "type": "p",
      "text": "Keep an agent key in `ARETTIC_API_KEY` on the machine that runs the agent, and an org key in a variable of your own. Don't put either in a repository, a browser or a URL."
    },
    {
      "type": "h2",
      "id": "errors",
      "text": "Errors"
    },
    {
      "type": "p",
      "text": "Every error is `{ \"error\": { \"code\", \"message\", \"doc_url\", \"retryable\" } }`. Retry only when `retryable` is true. The codes the flows on this page can answer with:"
    },
    {
      "type": "table",
      "caption": "Error codes on this page",
      "head": [
        "Code",
        "HTTP",
        "Meaning",
        "Fix"
      ],
      "rows": [
        [
          "[unauthenticated](/docs/errors#unauthenticated)",
          "401",
          "No valid key or session was sent, or the key was revoked.",
          "Send `Authorization: Bearer <key>` with an active key."
        ],
        [
          "[forbidden](/docs/errors#forbidden)",
          "403",
          "You're signed in but your role can't do this.",
          "Ask an org owner."
        ],
        [
          "[ip_not_allowed](/docs/errors#ip_not_allowed)",
          "403",
          "The agent key has an IP allowlist and this request came from elsewhere.",
          "Call from an allowed IP or update the allowlist."
        ],
        [
          "[rate_limited](/docs/errors#rate_limited)",
          "429 (retry)",
          "Too many requests in a short time.",
          "Wait and retry. Limits are in the `RateLimit-*` headers."
        ],
        [
          "[invalid_token](/docs/errors#invalid_token)",
          "401",
          "A sign-in link or invite is invalid, used or expired.",
          "Request a new link."
        ],
        [
          "[not_configured](/docs/errors#not_configured)",
          "503",
          "This feature isn't switched on in this environment.",
          "Use another sign-in method."
        ],
        [
          "[unverified_email](/docs/errors#unverified_email)",
          "401",
          "Google hasn't verified this email address.",
          "Sign in with an email link instead."
        ],
        [
          "[invalid_state](/docs/errors#invalid_state)",
          "400",
          "The Google sign-in took too long or was opened in another browser.",
          "Start Google sign-in again."
        ],
        [
          "[google_failed](/docs/errors#google_failed)",
          "401 (retry)",
          "Google didn't complete the sign-in.",
          "Try again, or use an email link."
        ]
      ]
    },
    {
      "type": "code",
      "title": "Response (example)",
      "lang": "json",
      "code": "{\n  \"error\": {\n    \"code\": \"unauthenticated\",\n    \"message\": \"Invalid or revoked API key\",\n    \"doc_url\": \"https://arettic.com/docs/errors#unauthenticated\",\n    \"retryable\": false\n  }\n}"
    },
    {
      "type": "h3",
      "id": "wrong-credential",
      "text": "The wrong credential on an endpoint"
    },
    {
      "type": "table",
      "caption": "What each mismatch answers",
      "head": [
        "You sent",
        "To",
        "Answer"
      ],
      "rows": [
        [
          "nothing",
          "an agent endpoint",
          "401 `unauthenticated`: Send your agent key as `Authorization: Bearer sk_…`"
        ],
        [
          "nothing",
          "an account or org endpoint",
          "401 `unauthenticated`: Sign in first"
        ],
        [
          "an agent key",
          "an account or org endpoint",
          "401 `unauthenticated`: Sign in first"
        ],
        [
          "a session token",
          "an agent endpoint",
          "401 `unauthenticated`: Invalid or revoked API key"
        ],
        [
          "a revoked key, or the key of a disabled agent",
          "an agent endpoint",
          "401 `unauthenticated`: Invalid or revoked API key"
        ],
        [
          "an agent key from an address outside its allowlist",
          "an agent endpoint",
          "403 `ip_not_allowed`"
        ],
        [
          "an org key",
          "anything outside `/v1/orgs/{orgId}/…` for its org, including agent endpoints and `/v1/me`",
          "403 `forbidden`"
        ],
        [
          "an org key",
          "`/v1/orgs/{orgId}/keys…`, or `POST /v1/orgs/{orgId}/deletion-requests`",
          "403 `forbidden`"
        ],
        [
          "a read org key",
          "a `POST`, `PUT`, `PATCH` or `DELETE`",
          "403 `forbidden`: This org key is read-only"
        ],
        [
          "an unknown or revoked org key",
          "anything",
          "401 `unauthenticated`: Unknown or revoked org key"
        ],
        [
          "a member's session",
          "an owner-only action: make or revoke org keys, invite, change roles",
          "403 `forbidden`: Only an org owner can do this"
        ]
      ]
    },
    {
      "type": "h2",
      "id": "related",
      "text": "Related"
    },
    {
      "type": "list",
      "items": [
        "[Quickstart](/docs/quickstart): from a key to a receipt.",
        "[Test mode](/docs/test-mode): what `sk_test_` keys return.",
        "[Org API](/docs/org-api): every endpoint an org key can call.",
        "[MCP](/docs/mcp): the hosted server and the bridge.",
        "[SDKs](/docs/sdks): the TypeScript and Python clients.",
        "[Rate limits](/docs/rate-limits): every limit and how to back off.",
        "[Error codes](/docs/errors): the full registry."
      ]
    }
  ]
}