Authentication
Every request carries an API key as a bearer token. There are no cookies, no sessions, and no OAuth dance — a key is the whole credential.
curl https://api.agentency.com/v1/me \
-H "Authorization: Bearer <YOUR_KEY>"
Only two endpoints work without one: GET /v1/status and
GET /v1/openapi.json.
Anatomy of a key
agy_live_9fK2mQ7pXr4TzB1c_a8Vt3nLd0sYhE5wUgJ6RmPq2XcFbN7k
└──┬───┘ └──────┬───────┘ └──────────────┬──────────────┘
prefix key id secret- Prefix —
agy_live_oragy_test_. Tells you at a glance what you are holding. - Key id — public, identifies the key. Safe to log.
- Secret — the part that authenticates. Never logged, never recoverable.
Only a hash of the secret is stored, so nobody can read it back — not you, not support. Lose it and you rotate.
Test and live
agy_test_ | agy_live_ | |
|---|---|---|
| Message credits | Not spent | Spent |
| Rate limits | Tighter | Full |
| Data | Your real account | Your real account |
A test key reads and writes your real account — it just does not spend live credits. It is for developing safely, not for throwing away data. Create a separate chatbot if you want somewhere disposable to experiment.
See Test mode.
What a key acts as
A key belongs to an account and always acts as that account, regardless of who
was signed in when it was created. That is why X-Account-Id is rejected when
it disagrees with the key: a key cannot hop between accounts, so a leaked key
is scoped to exactly one.
GET /v1/me tells a running process which account and key it is using:
curl https://api.agentency.com/v1/me \
-H "Authorization: Bearer <YOUR_KEY>"{
"object": "account",
"id": 1,
"name": "Acme Support",
"key": {
"object": "api_key",
"id": 1,
"mode": "test",
"key_id": "9fK2mQ7pXr4TzB1c",
"masked": "agy_test_9fK2mQ7pXr4TzB1c_••••bN7k",
"scopes": ["chatbots:read", "chat:send"],
"last_used_at": "2026-01-01T00:00:00+00:00"
}
}It needs no scope, which makes it the right health check for a credential.
Sending it
curl https://api.agentency.com/v1/chatbots \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Accept: application/json"The header is the only accepted transport. Keys are not read from query strings, because query strings end up in access logs, browser history and referrer headers.
Rotation
Rotating issues a new secret and keeps the old one working for 24 hours, so you can deploy without a flag day.
- Rotate in the dashboard. Store the new secret.
- Deploy. Both secrets are accepted, so nothing fails mid-rollout.
- Confirm traffic is using the new key (`last_used_at` on the old one stops moving).
- Do nothing — the old secret expires by itself.
Scopes cannot be changed on an existing key. To change them, mint a new key, deploy, then revoke the old one.
IP allowlists
A key can be restricted to specific addresses or CIDR ranges. A request from anywhere else is rejected even with the correct secret.
Worth doing for a key that lives on fixed infrastructure. Not worth doing for one on ephemeral workers, where you will spend more time chasing IP changes than the restriction is worth.
When a key leaks
- Revoke it immediately — the dashboard, or DELETE /v1/api_keys/{id} for the calling key. Revocation is instant.
- Mint a replacement with the same scopes and deploy it.
- Read GET /v1/request_logs for calls you do not recognise.
- Check the audit trail for anything created or deleted while it was exposed.
Anything shipped to a browser is public — bundled JavaScript, a mobile app binary, a public repository. Call the API from your server. For in-page chat, use the widget, which is designed to be public and carries no account credentials.
Why a 401 happened
401 UNAUTHENTICATED covers several causes. In order of likelihood:
- No
Authorizationheader, or notBearer <key>. - The key was revoked.
- It expired.
- Its rotation grace window ended.
- The request came from an address outside its allowlist.
- A live key against a test-only surface, or the reverse.
GET /v1/request_logs records these when the caller proved possession of the
secret, so the log usually answers "why did my key stop working" directly.