Skip to main content

Bot Identity

Bots are service accounts that authenticate against the Entity API using a name and a long-lived secret. They receive short-lived JWTs (1 hour TTL by default) and carry scoped permissions defined at registration time.

Use bots when you need a persistent, non-human identity — for example, a webhook handler, a scheduled sync job, or a secondary agent running inside a multi-agent pipeline.


When to use bots vs other auth methods​

MethodBest forPermissions
API Key (fyso_ak_*)MCP servers, admin scriptsFull tenant access
Platform Key (fyso_pkey_*)Public REST APIsRole-based matrix, static
Bot identityAutomated services, agentsExplicit entity-level, dynamic JWT
Tenant user tokenUser-facing appsRole-based

Choose bots over API keys when you want explicit, least-privilege scoping per service. Choose bots over Platform keys when the credential must be managed programmatically (register, rotate, revoke via API without touching the admin panel).


Registration​

Bot endpoints accept two caller types: an admin JWT (from POST /api/auth/login) or a tenant user JWT (from a user session). The only exception is reset-secret, which is admin-only.

Admin registration​

Admins register bots for any tenant they own. The request must include tenantSlug.

Step 1 — Log in as admin

curl -X POST https://api.fyso.dev/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@example.com",
"password": "your-password"
}'

# Response:
# { "success": true, "data": { "token": "eyJhbGci...", "admin": { ... } } }

Step 2 — Register the bot

curl -X POST https://api.fyso.dev/api/auth/bots/register \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "inventory-agent",
"tenantSlug": "my-workspace",
"permissions": {
"entities": {
"products": ["read", "update"],
"inventory": ["create", "read", "update", "delete"]
}
}
}'

# Response (201):
# {
# "success": true,
# "data": {
# "id": "a1b2c3d4-...",
# "name": "inventory-agent",
# "secret": "dK9mXqP3nR8vTw2zAe7yLs4cBhJfGu6i",
# "tenantSlug":"my-workspace"
# }
# }

The secret is shown exactly once. Store it in a secret manager or environment variable immediately.

User self-registration​

Tenant users can also register bots scoped to their own tenant. The tenantSlug field is not required — the tenant is resolved from the user's session. API key authentication (fyso_ak_*) is not accepted for this flow; a user JWT is required.

Permissions requested for the bot must be a strict subset of the user's own permissions. A user cannot grant a bot more access than they themselves have.

curl -X POST https://api.fyso.dev/api/auth/bots/register \
-H "Authorization: Bearer USER_JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "sync-agent",
"permissions": {
"entities": {
"contacts": ["read", "update"]
}
}
}'

# Response (201):
# {
# "success": true,
# "data": {
# "id": "a1b2c3d4-...",
# "name": "sync-agent",
# "secret": "dK9mXqP3nR8vTw2zAe7yLs4cBhJfGu6i",
# "tenantId": "f8e7d6c5-..."
# }
# }

Errors specific to user registration:

StatusCondition
403Requested permissions exceed the user's own scope, or wildcard entity requested
403API key used instead of user JWT
429Per-user bot limit reached (default: 5 active bots per user)

Permissions​

Permissions follow this structure:

{
"entities": {
"<entity-name>": ["create", "read", "update", "delete"]
}
}

Each entry maps an entity name (the slug used in API paths) to an array of allowed CRUD actions. You can include any subset of the four actions.

Constraints:

  • All four actions (create, read, update, delete) are the only valid values. Any other string returns a 400.
  • Wildcard entity names (e.g., "*") are not allowed for bots. You must list each entity explicitly.
  • permissions is optional at registration. If omitted, the bot has no entity access until you reset and re-register with permissions.

Example — read-only bot:

{
"entities": {
"contacts": ["read"],
"deals": ["read"]
}
}

Example — write-only ingestion bot:

{
"entities": {
"events": ["create"]
}
}

Identify — getting a JWT​

Once registered, a bot identifies itself to receive a short-lived JWT. The caller must be the admin or user who created the bot:

curl -X POST https://api.fyso.dev/api/auth/bots/identify \
-H "Authorization: Bearer ADMIN_OR_USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "inventory-agent",
"secret": "dK9mXqP3nR8vTw2zAe7yLs4cBhJfGu6i"
}'

# Response (200):
# {
# "success": true,
# "data": {
# "id": "a1b2c3d4-...",
# "name": "inventory-agent",
# "tenantSlug": "my-workspace",
# "tenantId": "f8e7d6c5-...",
# "permissions": { "entities": { "products": ["read", "update"], ... } },
# "token": "eyJhbGci...",
# "expiresIn": 3600
# }
# }

Use the JWT on entity endpoints​

curl https://api.fyso.dev/api/entities/products/records \
-H "Authorization: Bearer BOT_JWT"

curl -X POST https://api.fyso.dev/api/entities/inventory/records \
-H "Authorization: Bearer BOT_JWT" \
-H "Content-Type: application/json" \
-d '{ "sku": "ABC-123", "quantity": 50 }'

Requests that exceed the bot's declared permissions receive 403 Forbidden.


JWT refresh pattern​

The JWT has a 1 hour TTL (expiresIn: 3600). Re-identify when the token expires. Pattern for long-running services:

let botToken: string | null = null;
let tokenExpiresAt: number = 0;

async function getBotToken(): Promise<string> {
// Re-identify if token is missing or within 60s of expiry
if (!botToken || Date.now() / 1000 >= tokenExpiresAt - 60) {
const res = await fetch("https://api.fyso.dev/api/auth/bots/identify", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.ADMIN_TOKEN}`,
},
body: JSON.stringify({
name: process.env.BOT_NAME,
secret: process.env.BOT_SECRET,
}),
});

const json = await res.json();
if (!json.success) throw new Error(`Bot identify failed: ${json.error}`);

botToken = json.data.token;
tokenExpiresAt = Math.floor(Date.now() / 1000) + json.data.expiresIn;
}

return botToken!;
}

// Usage:
const token = await getBotToken();
const response = await fetch("https://api.fyso.dev/api/entities/products/records", {
headers: { "Authorization": `Bearer ${token}` },
});

The 60-second buffer avoids race conditions where the token expires mid-request.


Lockout behavior​

Failed identify calls (wrong secret) are tracked per bot:

Failed attemptsLockout duration
1-4No lockout
51 minute
65 minutes
730 minutes
81 hour
9+2 hours

A successful identify resets the counter. If your bot is locked, wait for the lockout to clear before retrying — additional failed attempts extend the lockout.


Listing bots​

Admins see all bots in the tenant. Users see only bots they created.

curl https://api.fyso.dev/api/auth/bots \
-H "Authorization: Bearer ADMIN_OR_USER_TOKEN"

# Response:
# {
# "success": true,
# "data": [
# {
# "id": "a1b2c3d4-...",
# "name": "inventory-agent",
# "tenantSlug": "my-workspace",
# "isActive": true,
# "lastSeenAt": "2026-03-11T10:30:00Z",
# "permissions": { "entities": { ... } },
# "createdAt": "2026-03-01T09:00:00Z"
# }
# ]
# }

Revocation​

Revoking a bot immediately invalidates further identify calls. Active JWTs remain valid until their TTL expires (max 1 hour).

curl -X POST https://api.fyso.dev/api/auth/bots/BOT_ID/revoke \
-H "Authorization: Bearer ADMIN_TOKEN"

# Response:
# { "success": true, "data": { "revoked": true } }

Revocation is permanent. To re-enable, register a new bot.


Secret rotation​

Reset a bot's secret when it is compromised or as part of regular key rotation. The old secret stops working immediately.

curl -X POST https://api.fyso.dev/api/auth/bots/BOT_ID/reset-secret \
-H "Authorization: Bearer ADMIN_TOKEN"

# Response:
# {
# "success": true,
# "data": {
# "id": "a1b2c3d4-...",
# "name": "inventory-agent",
# "secret": "newSecretValueShownOnce"
# }
# }

Only active bots can have their secret reset. Revoked bots return 404.

reset-secret is admin-only. There is no user-level equivalent.


Security considerations​

Secrets: Stored as bcrypt hashes server-side. The plaintext value is returned only once at registration or reset. There is no recovery path — if lost, reset the secret.

JWT scope: Bot JWTs carry "aud": "fyso-bot" and "scope": "bot". They are validated separately from user tokens and API keys. A bot JWT cannot be used to access admin endpoints.

Least privilege: Register each bot with the minimum set of entities and actions it actually needs. A bot with ["read"] on contacts cannot write to contacts or access invoices, even if the tenant has those entities.

No wildcards: Unlike Platform API keys, bot permissions cannot use "*" to grant access to all entities. This is intentional — bots are meant to have narrow, explicit scopes.

Revoke before you delete: If a bot's credentials are leaked, revoke immediately. Active JWTs cannot be invalidated before their TTL, but revocation stops new JWTs from being issued.


API reference​

POST /api/auth/bots/register​

Register a new bot.

Auth: Admin JWT or tenant user JWT. API keys are not accepted.

Request body:

FieldTypeRequiredDescription
namestringYesBot name. 3-50 chars, lowercase, alphanumeric and hyphens. Cannot start or end with a hyphen. Must be unique within the tenant.
tenantSlugstringAdmin onlySlug of the tenant this bot belongs to. Resolved from session for user callers.
permissionsobjectNoEntity permissions map. See Permissions. User callers: must not exceed the caller's own permissions.

Response (201) — admin caller: { id, name, secret, tenantSlug }. Secret shown once.

Response (201) — user caller: { id, name, secret, tenantId }. Secret shown once.


POST /api/auth/bots/identify​

Authenticate a bot and receive a JWT.

Auth: Admin JWT or tenant user JWT.

Request body:

FieldTypeRequiredDescription
namestringYesBot name.
secretstringYesBot secret.

Response (200):

{
"success": true,
"data": {
"id": "uuid",
"name": "inventory-agent",
"tenantSlug": "my-workspace",
"tenantId": "uuid",
"permissions": { "entities": { "products": ["read", "update"] } },
"token": "eyJhbGci...",
"expiresIn": 3600
}
}

Error responses:

StatusCondition
401Bot not found, wrong secret, or lockout active
403Bot has been revoked

GET /api/auth/bots​

List bots.

Auth: Admin JWT or tenant user JWT.

  • Admin: returns all bots in the tenant (including user-created bots).
  • User: returns only bots created by that user.

Response (200): Array of bot objects (no secret field).


POST /api/auth/bots/:id/revoke​

Revoke a bot permanently.

Auth: Admin JWT or tenant user JWT.

  • Admin: can revoke any bot in the tenant.
  • User: can only revoke bots they created.

Response (200):

{ "success": true, "data": { "revoked": true } }

Returns 404 if the bot does not exist or belongs to a different admin.


POST /api/auth/bots/:id/reset-secret​

Generate a new secret for an active bot.

Auth: Admin JWT required. Admin-only — no user-level equivalent.

Response (200):

{
"success": true,
"data": {
"id": "uuid",
"name": "inventory-agent",
"secret": "new-plaintext-secret-shown-once"
}
}

Returns 404 if the bot is revoked or not found.