Saltar al contenido principal

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.