Getting started

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

The three credentials, side by side
Starts withCredentialHeld byReachesMade
sk_test_… or sk_live_…Agent keyOne agentThe buying endpoints: recommend, execute, jobs, receipts, disputes, approvals, balance, GET /v1/agent and the MCP serverOn the Agents page, or with POST /v1/orgs/{orgId}/agents
ok_…Org keyA script or agent that runs the orgOnly /v1/orgs/{orgId}/… for its own org. A read key only reads. Never key management or a data-deletion requestOn the Team page by a signed-in owner, or with POST /v1/orgs/{orgId}/keys
ss_… or the arettic_session cookieSessionA signed-in person, or a script acting as oneEvery account and org endpoint: /v1/me, POST /v1/orgs, /v1/orgs/{orgId}/…, POST /auth/logoutPOST /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.

Header
Authorization: Bearer sk_test_…
curl
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

  1. 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.
  2. 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.
  3. 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
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"}'
TypeScript
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 only
Python
import 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
Response (example)
{
  "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

Endpoints that take an agent key
EndpointWhat it does
POST /v1/recommendRanked tools for a task. See recommend.
POST /v1/executeRun 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/receiptsThe 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/disputesDispute 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/balanceThe agent's month-to-date spend, its budget and the org's credits.
GET /v1/agentThe calling agent and its limits.
POST /mcpThe 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
curl https://api.arettic.com/v1/agent \
  -H "Authorization: Bearer $ARETTIC_API_KEY"
TypeScript
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);
Python
from arettic import Arettic

client = Arettic()  # reads ARETTIC_API_KEY
agent = client.me()["agent"]
print(agent["name"], agent["mode"], agent["monthly_budget"]["usd"])
Response (example)
{
  "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.

MCP config
{
  "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.

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
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"}'
Response (example)
{
  "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.

Response (example)
{
  "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
curl https://api.arettic.com/v1/orgs/$ORG_ID/agents \
  -H "Authorization: Bearer ok_…"
TypeScript
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]));
Python
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

  1. 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.
  2. 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.
  3. 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.
  4. A session lasts 30 days. POST /auth/logout ends it early.
curl
curl -X POST https://api.arettic.com/auth/email/start \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]"}'
Response (example)
{ "status": "sent" }
curl
# 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": "…"}'
Response (example)
{
  "session_token": "ss_…",
  "user_id": "0d9e8f7a-6b5c-4d3e-9f1a-0b9c8d7e6f5a",
  "new_user": false
}
curl
curl https://api.arettic.com/v1/me \
  -H "Authorization: Bearer ss_…"
Response (example)
{
  "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" }
  ]
}
TypeScript
// 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());
Python
# 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.

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.

curl
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_…"
TypeScript
const { key } = await org.agents.rotateKey(agentId); // the old key stopped working
await org.agents.revokeKey(agentId); // { status: "revoked" }
Python
key = org.agents.rotate_key(agent_id)["key"]  # the old key stopped working
org.agents.revoke_key(agent_id)  # {"status": "revoked"}
Response (example)
{ "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
curl -X DELETE https://api.arettic.com/v1/orgs/$ORG_ID/keys/$KEY_ID \
  -H "Authorization: Bearer ss_…"
Response (example)
{ "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
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.

curl
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"]}'
TypeScript
const agent = await org.agents.update(agentId, {
  ip_allowlist: ["203.0.113.7", "203.0.113.0/24"],
});
console.log(agent.ip_allowlist);
Python
agent = org.agents.update(agent_id, ip_allowlist=["203.0.113.7", "203.0.113.0/24"])
print(agent["agent"]["ip_allowlist"])
Response (example)
{
  "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"
  }
}
Response from another address (example)
{
  "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

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:

Error codes on this page
CodeHTTPMeaningFix
unauthenticated401No valid key or session was sent, or the key was revoked.Send Authorization: Bearer <key> with an active key.
forbidden403You're signed in but your role can't do this.Ask an org owner.
ip_not_allowed403The agent key has an IP allowlist and this request came from elsewhere.Call from an allowed IP or update the allowlist.
rate_limited429 (retry)Too many requests in a short time.Wait and retry. Limits are in the RateLimit-* headers.
invalid_token401A sign-in link or invite is invalid, used or expired.Request a new link.
not_configured503This feature isn't switched on in this environment.Use another sign-in method.
unverified_email401Google hasn't verified this email address.Sign in with an email link instead.
invalid_state400The Google sign-in took too long or was opened in another browser.Start Google sign-in again.
google_failed401 (retry)Google didn't complete the sign-in.Try again, or use an email link.
Response (example)
{
  "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

What each mismatch answers
You sentToAnswer
nothingan agent endpoint401 unauthenticated: Send your agent key as Authorization: Bearer sk_…
nothingan account or org endpoint401 unauthenticated: Sign in first
an agent keyan account or org endpoint401 unauthenticated: Sign in first
a session tokenan agent endpoint401 unauthenticated: Invalid or revoked API key
a revoked key, or the key of a disabled agentan agent endpoint401 unauthenticated: Invalid or revoked API key
an agent key from an address outside its allowlistan agent endpoint403 ip_not_allowed
an org keyanything outside /v1/orgs/{orgId}/… for its org, including agent endpoints and /v1/me403 forbidden
an org key/v1/orgs/{orgId}/keys…, or POST /v1/orgs/{orgId}/deletion-requests403 forbidden
a read org keya POST, PUT, PATCH or DELETE403 forbidden: This org key is read-only
an unknown or revoked org keyanything401 unauthenticated: Unknown or revoked org key
a member's sessionan owner-only action: make or revoke org keys, invite, change roles403 forbidden: Only an org owner can do this

Updated 2026-09-29 · This page as Markdown · JSON