Authentication
API keys and their levels, acting as a user with X-User-Email, certificate JWTs for the Node SDK, what each credential can do, errors and rate limits.
Every request to https://api.agnt.ai carries one credential in Authorization: Bearer <credential>. The credential decides which account the request belongs to and, for anything a user owns, which single user it acts as. That binding keeps each end user's data away from every other end user of your app.
How it works
| Credential | Format | Use it for |
|---|---|---|
| API key | ak_live_ followed by 64 hex characters | Server-to-server calls from your backend, curl and the agnt CLI. Covers the full public API. |
| Certificate JWT | An RS256 JWT with a kid header, signed with your private key | @agnt-sdk/client |
request --> credential --> account --> user (for user resources)
|
+-- account key ........ account-wide; add X-User-Email to act as a user
+-- organization key ... one organization, for its admin operations
+-- user key ........... exactly one user, always
Data a user owns (tasks, chats, memories, contacts, calendars, files, pending approvals) is isolated per user, not just per app. A user-scoped key acts only as its own user for approvals and edits, and an account key with X-User-Email acts as that user. An account key without the header is your server's trusted, account-wide credential: it cannot act as a user, but it can list, read and delete tasks and chats across all users.
Create an API key
In the console, open Settings > API Keys and generate a key. The full key is shown once. AGNT stores only a hash of it, so a lost key cannot be recovered: revoke it and create another.
You can also create keys through the API:
curl https://api.agnt.ai/api-keys \
-X POST \
-H "Authorization: Bearer $AGNT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "label": "harborview-billing", "expiresAt": "2027-11-02T00:00:00.000Z" }'
{
"ok": true,
"apiKey": {
"id": "6702cb37e8f9a0b1c2d3e4f5",
"account": "66f1a2b3c4d5e6f708192a3b",
"organization": null,
"user": null,
"label": "harborview-billing",
"prefix": "ak_live_3f9c2a1b",
"scopes": [],
"status": "active",
"lastUsedAt": null,
"expiresAt": "2027-11-02T00:00:00.000Z",
"createdAt": "2026-11-02T16:24:11.000Z",
"key": "ak_live_..."
}
}
key appears in this response only. Afterwards the key is identified by id and prefix.
| Name | Type | Required | Description |
|---|---|---|---|
label | string | Yes | A name for your records |
userId | string | No | Create a user key for this user. Creating one for someone other than yourself requires an organization or account admin. |
expiresAt | string | No | ISO 8601. The key stops authenticating at this time. |
scopes | string[] | No | Strings stored on the key and returned on reads |
Key levels
| Level | Created by | Reaches |
|---|---|---|
| Account | A developer on your account (console, or an account key) | The whole account: users, teams, organizations, assistants, skills. Acts as any user with X-User-Email. |
| Organization | An admin of one of your customer organizations, from their organization settings | Administrative operations for that organization |
| User | The user, or an admin on their behalf | Acts only as that user for approvals and edits |
The level is fixed when the key is created. Use the narrowest level that works: a backend job that serves one user holds a user key, not an account key.
Act as a user
User resources need a user. With an account key, name the user in X-User-Email:
curl "https://api.agnt.ai/tasks?perPage=20" \
-H "Authorization: Bearer $AGNT_API_KEY" \
-H "X-User-Email: [email protected]"
- The email must belong to a user in your account (any of the user's identifiers). Otherwise the request fails with
401. It never falls back to account-wide access. - Without the header, an account key reaches account resources. Calls that need a user, such as
POST /tasks, fail. - A user key always acts as its own user. Do not send
X-User-Emailwith it. - Before a key can act as a user for the first time, the user needs a timezone and working hours, confirmed with
POST /onboarding/mark-onboarded. Until then user calls return428 onboarding_required. The Quickstart shows the three calls.
Inspect a key
GET /me returns what the key resolves to:
curl https://api.agnt.ai/me \
-H "Authorization: Bearer $AGNT_API_KEY"
{ "ok": true, "accountId": "66f1a2b3c4d5e6f708192a3b", "email": null, "orgId": null, "userId": null, "scopes": [] }
An account key has orgId and userId set to null. An organization key has orgId. A user key has userId and email.
Revoke and delete a key
GET /api-keys lists the keys you can manage, never the secret. Revocation takes effect immediately: the next request with the key fails with 401.
curl -X POST https://api.agnt.ai/api-keys/6702cb37e8f9a0b1c2d3e4f5/revoke \
-H "Authorization: Bearer $AGNT_API_KEY"
Only a revoked key can be deleted:
curl -X DELETE https://api.agnt.ai/api-keys/6702cb37e8f9a0b1c2d3e4f5 \
-H "Authorization: Bearer $AGNT_API_KEY"
Revoking a key that is already revoked returns 400. A key cannot be deleted while it is active, or while your hosted app uses it.
Rotate a key
- Create the new key.
- Deploy it to every caller.
- Revoke the old key. Its
lastUsedAtinGET /api-keysshows whether anything still uses it.
Certificate JWTs for the SDK
The Node SDK, @agnt-sdk/client, authenticates by signing short-lived JWTs with a private key that never leaves your servers. Register the matching public key in the AGNT console to get its key id (kid). AGNT holds only the public key.
Install the SDK (ESM, Node.js 18 or later):
npm install @agnt-sdk/client
Configure it in agnt.config.js at your project root. AgntClient.create() searches up from the working directory for this file.
export default {
privateKey: process.env.AGNT_PRIVATE_KEY, // PKCS#8 PEM
kid: process.env.AGNT_KID,
apiUrl: 'https://api.agnt.ai',
};
AgntClient.create() returns a management client with an account-level token. client.as(email) returns a delegated client whose token carries that user's email. Calls through it currently act account-wide.
import { AgntClient, AgntApiError } from '@agnt-sdk/client';
const client = await AgntClient.create();
const jane = client.as('[email protected]', { firstName: 'Jane', lastName: 'Rivera' });
try {
const assistants = await client.assistants.list();
} catch (err) {
if (err instanceof AgntApiError) console.error(err.status, err.errorCode, err.message);
else throw err;
}
Token contents
| Part | Value |
|---|---|
| Header | { "alg": "RS256", "kid": "<your kid>" } |
| Claims | iat, exp (one hour after iat) |
| Delegated claims | email, plus firstName and lastName when passed to client.as(email, { firstName, lastName }) |
The SDK caches each token per kid and email and signs a new one when less than five minutes remain.
- A certificate authenticates only its own account. The account comes from the registered certificate, never from claims in the token.
- Tokens with a
typeclaim are refused. - The certificate must be active. A revoked certificate fails with
401.
What each credential can do
| Operation | Account key | Account key + X-User-Email | User key |
|---|---|---|---|
| Provision users, manage rosters, teams and organizations | Yes | Not needed | No |
| Create and run tasks and chats; read and write memories | No: name a user | Yes, as that user | Yes, as its user |
| List, read and delete tasks and chats across all users | Yes (GET /tasks, GET /tasks/:taskId, DELETE /tasks/:taskId, GET /chats, GET /chats/:chatId, DELETE /chats/:chatId; narrow a list to one user with ownerEmail on tasks or userEmail on chats) | No, only that user's | No, only its user's |
| Approve, edit or cancel a pending action | No | Yes, relaying that user's decision | Yes |
These limits apply whatever the credential:
- Approvals need a person. Only the user a pending action belongs to can approve it: from their own signed-in session, or through your backend relaying their decision with an API key acting as them. A certificate token cannot approve, edit or cancel one (
403), and neither can a console session acting on a user's behalf. - Some account settings are console-only. Account-wide defaults such as the default model tiers (Settings > Model) are changed in the console by an account admin.
- A blocked user is blocked everywhere. When a user's status is
inactiveorsuspended, every request acting as them fails with403 account_blocked. - A frozen account accepts no new work. While the account-wide kill switch is engaged, creating, running or messaging tasks returns
423witherror_code: "KILL_SWITCH_ENGAGED".
Errors
API errors share one shape:
{ "status": 404, "error_code": "not_found", "error": "Task not found" }
An invalid API key and a rate-limited request are refused before they reach an endpoint, with this shape:
{ "ok": false, "error": "Too many requests", "retryAfter": 1793636711 }
| Status | error_code | When | What to do |
|---|---|---|---|
400 | bad_request (or a specific code) | Validation failed; error lists each field and problem | Fix the request |
401 | unauthorized | Missing or malformed credential; expired token; revoked or unknown certificate; X-User-Email that matches no user in the account | Check the credential and the email |
401 | none ({ "ok": false } body) | Unknown, revoked or expired API key | Use a valid key |
403 | forbidden | Authenticated but not allowed: wrong role, assistant not on the user's roster, approval from a certificate token or from an account key with no X-User-Email | Use a credential with the right level |
403 | account_blocked | The user is inactive or suspended | Unblock the user, or stop acting as them |
404 | not_found | The resource does not exist, or is not visible to this user | Check the id and the user |
409 | conflict (or a specific code) | Conflicts with current state, such as a concurrent change | Re-read, then retry |
423 | KILL_SWITCH_ENGAGED | The account is frozen by the kill switch | A person signed in to your hosted app releases the freeze |
428 | onboarding_required | The user has not finished setup | Set timezone and working hours, then POST /onboarding/mark-onboarded |
429 | none ({ "ok": false } body) | Rate limited | Wait Retry-After seconds, then retry |
500 | server_error | Unexpected failure | Retry with exponential backoff |
A resource that belongs to another user returns 404 rather than 403, so its existence is not disclosed.
Errors on every user call
Any call made as a user (an account key with X-User-Email, or a user key) can return these, whatever the endpoint. The API reference does not repeat them on each operation.
| Status | error_code | When |
|---|---|---|
401 | unauthorized | X-User-Email matches no user in the account |
403 | account_blocked | The user's status is inactive or suspended |
428 | onboarding_required | The user has not finished setup. Only the calls that finish it still work: GET and PATCH /profile, GET and PUT /users/:userId/preferences/scheduling, POST /onboarding/mark-onboarded, and listing, revoking and deleting API keys. |
423 | KILL_SWITCH_ENGAGED | Creating or running a task (POST /tasks, POST /tasks/:taskId/process) while the account is frozen |
An endpoint that needs a user, called with an account key and no X-User-Email, returns the error listed on that operation: usually 401 unauthorized, or 403 or 404 where the call checks the task's owner or the caller's access.
Rate limits
API-key traffic is rate limited per account, client IP and path: 1,000 requests a minute for reads (GET) and 50 a minute for writes by default. A limited request returns 429 with these headers. The body's retryAfter is the Unix time at which you may retry.
| Header | Description |
|---|---|
Retry-After | Seconds until you may retry |
X-RateLimit-Limit | The limit for this window |
X-RateLimit-Remaining | 0 on a limited response |
X-RateLimit-Reset | Unix time, in seconds, when the window resets |
Read the headers rather than hard-coding the numbers.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /me | What the API key resolves to |
GET | /api-keys | List keys (never the secret) |
POST | /api-keys | Create a key; the secret is returned once |
POST | /api-keys/:keyId/revoke | Revoke immediately |
DELETE | /api-keys/:keyId | Delete a revoked key |
Best practices
- Send credentials only in the
Authorizationheader, never in a URL or query string. - Unknown fields in a request body are ignored. Check field names against the API reference.
- Keep account keys on your servers. If your app calls AGNT from a browser or mobile client, route the call through your backend, which holds the key and sets
X-User-Emailfrom your own session, never from client input.
See also
- Quickstart: use a key end to end
- Users, organizations and teams: provisioning, SSO and SCIM
- Security: isolation, encryption and audit