Authentication and keys
Agent keys, org keys and sessions: what each can do, how to send them, and how to rotate or revoke them.
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.
Arettic is pre-launch. Keys go to design partners; everyone else can join the waitlist. The @arettic/sdk (npm), arettic (PyPI) and @arettic/mcp packages used in the samples are published at launch.
The three credentials
| Starts with | Credential | Held by | Reaches | Made |
|---|---|---|---|---|
| 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 |
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.
Send the header
Send the credential as a bearer token. The header is the same for all three kinds.
Authorization: Bearer sk_test_…
curl https://api.arettic.com/v1/agent \ -H "Authorization: Bearer sk_test_…"
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.
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.
Agent keys
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 says what a test key returns.
Get a key
- Sign in and open 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.
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.
curl -X POST https://api.arettic.com/v1/orgs/$ORG_ID/agents \
-H "Authorization: Bearer ok_…" \
-H "Content-Type: application/json" \
-d '{"name": "Research agent", "mode": "test"}'import { AretticOrg } from "@arettic/sdk";
const org = new AretticOrg({
apiKey: process.env.ARETTIC_ORG_KEY, // an ok_… key
orgId: process.env.ARETTIC_ORG_ID!,
});
const { agent, key } = await org.agents.create({ name: "Research agent", mode: "test" });
console.log(agent.id, key); // the key is in this answer onlyimport os from arettic import AretticOrg org = AretticOrg(api_key=os.environ["ARETTIC_ORG_KEY"], org_id=os.environ["ARETTIC_ORG_ID"]) made = org.agents.create(name="Research agent", mode="test") print(made["agent"]["id"], made["key"]) # the key is in this answer only
{
"agent": {
"id": "2f6c1c0e-5b1a-4d8e-9a3b-7c1d2e3f4a5b",
"name": "Research agent",
"mode": "test",
"status": "active",
"monthly_budget": { "credits": "50000", "usd": "50.000" },
"approval_threshold": { "credits": "20000", "usd": "20.000" },
"ip_allowlist": [],
"key_prefix": "sk_test_Ab12",
"key_last_used_at": null,
"created_at": "2026-09-28T14:02:11.000Z"
},
"key": "sk_test_…",
"key_note": "Shown once. Store it now; we keep only a hash."
}What an agent key reaches
| Endpoint | What it does |
|---|---|
| POST /v1/recommend | Ranked tools for a task. See recommend. |
| POST /v1/execute | Run a tool; charged only if the result passes its check. See execute. |
| GET /v1/jobs/{id} | A batch of more than 25 items: status, progress and per-item outcomes. See jobs. |
| GET /v1/receipts | The org's receipts, newest first. See 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. |
| 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. |
| 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. |
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.
Check a key
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.
curl https://api.arettic.com/v1/agent \ -H "Authorization: Bearer $ARETTIC_API_KEY"
import { Arettic } from "@arettic/sdk";
const arettic = new Arettic({ apiKey: process.env.ARETTIC_API_KEY });
const agent = await arettic.me();
console.log(agent.name, agent.mode, agent.monthly_budget.usd);from arettic import Arettic client = Arettic() # reads ARETTIC_API_KEY agent = client.me()["agent"] print(agent["name"], agent["mode"], agent["monthly_budget"]["usd"])
{
"agent": {
"id": "2f6c1c0e-5b1a-4d8e-9a3b-7c1d2e3f4a5b",
"name": "Research agent",
"mode": "test",
"status": "active",
"monthly_budget": { "credits": "50000", "usd": "50.000" },
"approval_threshold": { "credits": "20000", "usd": "20.000" },
"ip_allowlist": [],
"key_prefix": "sk_test_Ab12",
"key_last_used_at": "2026-09-29T09:41:00.000Z",
"created_at": "2026-09-28T14:02:11.000Z"
}
}Use the key with MCP
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 for the tools and the client setups.
{
"mcpServers": {
"arettic": {
"command": "npx",
"args": ["-y", "@arettic/mcp"],
"env": { "ARETTIC_API_KEY": "sk_test_…" }
}
}
}Rate limit
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.
Org keys
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. This section covers what the key is and what it can't do.
- It starts with
ok_and has a scope. Areadkey can only sendGETrequests. Awritekey 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 403forbidden, including the agent endpoints and/v1/me. - It can't manage keys.
/v1/orgs/{orgId}/keysneeds 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-requestsneeds 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.
Make an org key
Only an owner can make or revoke org keys, and only while signed in: on Team under Org API keys (a name, then read or write), or with a session token. A member's session answers 403 forbidden.
curl -X POST https://api.arettic.com/v1/orgs/$ORG_ID/keys \
-H "Authorization: Bearer ss_…" \
-H "Content-Type: application/json" \
-d '{"name": "Billing agent", "scope": "read"}'{
"key": {
"id": "5b7d9f1a-2c3e-4d5f-8a9b-0c1d2e3f4a5b",
"name": "Billing agent",
"scope": "read",
"prefix": "ok_Ab12Cd3",
"created_by_email": "[email protected]",
"created_at": "2026-09-29T10:20:00.000Z",
"last_used_at": null
},
"secret": "ok_…",
"note": "Shown once. Store it now; we keep only a hash."
}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.
{
"keys": [
{
"id": "5b7d9f1a-2c3e-4d5f-8a9b-0c1d2e3f4a5b",
"name": "Billing agent",
"scope": "read",
"prefix": "ok_Ab12Cd3",
"created_by_email": "[email protected]",
"created_at": "2026-09-29T10:20:00.000Z",
"last_used_at": "2026-09-29T11:02:00.000Z"
}
]
}Use an org key
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.
curl https://api.arettic.com/v1/orgs/$ORG_ID/agents \ -H "Authorization: Bearer ok_…"
import { AretticOrg } from "@arettic/sdk";
const org = new AretticOrg({
apiKey: process.env.ARETTIC_ORG_KEY,
orgId: process.env.ARETTIC_ORG_ID!,
});
const agents = await org.agents.list();
console.log(agents.map((a) => [a.name, a.mode, a.key_prefix]));import os
from arettic import AretticOrg
org = AretticOrg(api_key=os.environ["ARETTIC_ORG_KEY"], org_id=os.environ["ARETTIC_ORG_ID"])
for a in org.agents.list()["agents"]:
print(a["name"], a["mode"], a["key_prefix"])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.
Sessions
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.
Sign in by email
POST /auth/email/startwith 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 thearettic_sessioncookie and sends you to the dashboard. - For a script, take the
tokenfrom the link and send it toPOST /auth/email/verify. The answer holdssession_token(ss_…),user_idandnew_user. Send the token as a bearer token from then on. - A session lasts 30 days.
POST /auth/logoutends it early.
curl -X POST https://api.arettic.com/auth/email/start \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'{ "status": "sent" }# The token is the token= part of the link in the email.
curl -X POST https://api.arettic.com/auth/email/verify \
-H "Content-Type: application/json" \
-d '{"token": "…"}'{
"session_token": "ss_…",
"user_id": "0d9e8f7a-6b5c-4d3e-9f1a-0b9c8d7e6f5a",
"new_user": false
}curl https://api.arettic.com/v1/me \ -H "Authorization: Bearer ss_…"
{
"user": {
"id": "0d9e8f7a-6b5c-4d3e-9f1a-0b9c8d7e6f5a",
"email": "[email protected]",
"name": null,
"email_verified": true,
"phone": "+14155550132",
"phone_verified": true
},
"orgs": [
{ "id": "7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "name": "Acme", "role": "owner" }
]
}// No SDK for sessions: plain fetch.
const api = "https://api.arettic.com";
await fetch(api + "/auth/email/start", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email: "[email protected]" }),
});
// Take the token from the link in the email, then:
const verified = await fetch(api + "/auth/email/verify", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ token: process.env.ARETTIC_LOGIN_TOKEN }),
});
const { session_token } = await verified.json();
const me = await fetch(api + "/v1/me", {
headers: { authorization: "Bearer " + session_token },
});
console.log(await me.json());# No SDK for sessions: urllib from the standard library.
import json
import os
import urllib.request
API = "https://api.arettic.com"
def call(method, path, body=None, token=None):
data = None if body is None else json.dumps(body).encode()
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = "Bearer " + token
req = urllib.request.Request(API + path, data=data, method=method, headers=headers)
with urllib.request.urlopen(req) as res:
return json.load(res)
call("POST", "/auth/email/start", {"email": "[email protected]"})
# Take the token from the link in the email, then:
session = call("POST", "/auth/email/verify", {"token": os.environ["ARETTIC_LOGIN_TOKEN"]})
me = call("GET", "/v1/me", token=session["session_token"])
print(me["user"]["email"], [o["name"] for o in me["orgs"]])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.
Google sign-in
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.
- 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/startanswers 503not_configured. Use an email link.
The session cookie
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.
Rotate and revoke
Agent keys
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.
- Dashboard: 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-keyandPOST /v1/orgs/{orgId}/agents/{agentId}/revoke-key, with a session token or a write org key.rotate-keyalso issues a key for an agent that has none.
curl -X POST https://api.arettic.com/v1/orgs/$ORG_ID/agents/$AGENT_ID/rotate-key \ -H "Authorization: Bearer ok_…" curl -X POST https://api.arettic.com/v1/orgs/$ORG_ID/agents/$AGENT_ID/revoke-key \ -H "Authorization: Bearer ok_…"
const { key } = await org.agents.rotateKey(agentId); // the old key stopped working
await org.agents.revokeKey(agentId); // { status: "revoked" }key = org.agents.rotate_key(agent_id)["key"] # the old key stopped working
org.agents.revoke_key(agent_id) # {"status": "revoked"}{ "key": "sk_live_…", "key_note": "The previous key stopped working immediately." }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.
Org keys
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.
curl -X DELETE https://api.arettic.com/v1/orgs/$ORG_ID/keys/$KEY_ID \ -H "Authorization: Bearer ss_…"
{ "status": "revoked" }Sessions
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.
curl -X POST https://api.arettic.com/auth/logout \ -H "Authorization: Bearer ss_…"
IP allowlist
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.
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.
- Dashboard: 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}withip_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 400invalid_input.
curl -X PATCH https://api.arettic.com/v1/orgs/$ORG_ID/agents/$AGENT_ID \
-H "Authorization: Bearer ok_…" \
-H "Content-Type: application/json" \
-d '{"ip_allowlist": ["203.0.113.7", "203.0.113.0/24"]}'const agent = await org.agents.update(agentId, {
ip_allowlist: ["203.0.113.7", "203.0.113.0/24"],
});
console.log(agent.ip_allowlist);agent = org.agents.update(agent_id, ip_allowlist=["203.0.113.7", "203.0.113.0/24"]) print(agent["agent"]["ip_allowlist"])
{
"agent": {
"id": "2f6c1c0e-5b1a-4d8e-9a3b-7c1d2e3f4a5b",
"name": "Research agent",
"mode": "live",
"status": "active",
"monthly_budget": { "credits": "50000", "usd": "50.000" },
"approval_threshold": { "credits": "20000", "usd": "20.000" },
"ip_allowlist": ["203.0.113.7", "203.0.113.0/24"],
"key_prefix": "sk_live_Ab12",
"key_last_used_at": "2026-09-29T09:41:00.000Z",
"created_at": "2026-09-28T14:02:11.000Z"
}
}{
"error": {
"code": "ip_not_allowed",
"message": "This key can't be used from this IP address",
"doc_url": "https://arettic.com/docs/errors#ip_not_allowed",
"retryable": false
}
}Org keys and sessions have no allowlist.
How keys are stored
- 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_prefixon an agent (12 characters) andprefixon an org key (10 characters). They appear in the dashboard and inGET /v1/agent. - We record when a key was last used (
key_last_used_aton an agent,last_used_aton 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.
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.
Errors
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:
| Code | HTTP | Meaning | Fix |
|---|---|---|---|
| unauthenticated | 401 | No valid key or session was sent, or the key was revoked. | Send Authorization: Bearer <key> with an active key. |
| forbidden | 403 | You're signed in but your role can't do this. | Ask an org owner. |
| 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 | 429 (retry) | Too many requests in a short time. | Wait and retry. Limits are in the RateLimit-* headers. |
| invalid_token | 401 | A sign-in link or invite is invalid, used or expired. | Request a new link. |
| not_configured | 503 | This feature isn't switched on in this environment. | Use another sign-in method. |
| unverified_email | 401 | Google hasn't verified this email address. | Sign in with an email link instead. |
| invalid_state | 400 | The Google sign-in took too long or was opened in another browser. | Start Google sign-in again. |
| google_failed | 401 (retry) | Google didn't complete the sign-in. | Try again, or use an email link. |
{
"error": {
"code": "unauthenticated",
"message": "Invalid or revoked API key",
"doc_url": "https://arettic.com/docs/errors#unauthenticated",
"retryable": false
}
}The wrong credential on an endpoint
| You sent | To | Answer |
|---|---|---|
| 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 |
Related
- Quickstart: from a key to a receipt.
- Test mode: what
sk_test_keys return. - Org API: every endpoint an org key can call.
- MCP: the hosted server and the bridge.
- SDKs: the TypeScript and Python clients.
- Rate limits: every limit and how to back off.
- Error codes: the full registry.