Skip to main content
Arcanflows

Authentication

Manage user authentication, MFA, API keys, profiles, permissions, and user administration.

Overview

Arcanflows uses two authentication methods: JWT tokens from login (for the management API and the console) and Embed API keys (emb_, for calling a specific agent from your own code). This page covers every auth-related endpoint — registration, login, MFA, refresh, Embed API keys, user administration, profiles, and permissions.

Base URL: /api/v1


Auth Endpoints

These endpoints handle user registration, login, logout, token refresh, and account recovery. Auth routes are prefixed with /auth (no /api/v1 prefix).

POST /auth/register

Register a new user account.

bash
curl -X POST /auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "securePassword123!",
    "first_name": "Jane",
    "last_name": "Doe"
  }'

Response 201 Created:

json
{
  "id": "usr_abc123",
  "email": "[email protected]",
  "first_name": "Jane",
  "last_name": "Doe",
  "role": "agent_user",
  "email_verified": false,
  "message": "Verification email sent"
}

POST /auth/login

Authenticate and receive JWT tokens. If MFA is enabled on the account, the response will include mfa_required: true and a temporary mfa_token instead of full access tokens.

bash
curl -X POST /auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "securePassword123!"
  }'

Response 200 OK (no MFA):

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 900,
  "user": {
    "id": "usr_abc123",
    "email": "[email protected]",
    "role": "agent_user"
  }
}

Response 200 OK (MFA required):

json
{
  "mfa_required": true,
  "mfa_token": "mfa_tmp_xyz789",
  "message": "MFA verification required"
}

POST /auth/mfa/verify

Complete login by verifying the MFA code. Use the mfa_token received from /auth/login.

bash
curl -X POST /auth/mfa/verify \
  -H "Content-Type: application/json" \
  -d '{
    "mfa_token": "mfa_tmp_xyz789",
    "code": "482910"
  }'

Response 200 OK:

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 900
}

POST /auth/logout

Invalidate the current session tokens. Requires a valid access token.

bash
curl -X POST /auth/logout \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "message": "Successfully logged out"
}

POST /auth/refresh

Exchange a valid refresh token for a new access token.

bash
curl -X POST /auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
  }'

Response 200 OK:

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 900
}

GET /auth/me

Get the currently authenticated user.

bash
curl -X GET /auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "id": "usr_abc123",
  "email": "[email protected]",
  "first_name": "Jane",
  "last_name": "Doe",
  "role": "agent_developer",
  "tenant_id": "tnt_xyz789",
  "email_verified": true,
  "mfa_enabled": true,
  "created_at": "2025-08-10T14:30:00Z"
}

POST /auth/change-password

Change password for the currently authenticated user.

bash
curl -X POST /auth/change-password \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "current_password": "oldPassword123!",
    "new_password": "newSecurePassword456!"
  }'

Response 200 OK:

json
{
  "message": "Password changed successfully"
}

POST /auth/forgot-password

Send a password reset email. Always returns 200 regardless of whether the email exists (to prevent enumeration).

bash
curl -X POST /auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]"
  }'

Response 200 OK:

json
{
  "message": "If the email exists, a reset link has been sent"
}

POST /auth/reset-password

Reset password using a token received via email.

bash
curl -X POST /auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "token": "reset_tok_abc123",
    "new_password": "newSecurePassword456!"
  }'

Response 200 OK:

json
{
  "message": "Password reset successfully"
}

POST /auth/verify-email

Verify a user's email address using the token sent during registration.

bash
curl -X POST /auth/verify-email \
  -H "Content-Type: application/json" \
  -d '{
    "token": "verify_tok_def456"
  }'

Response 200 OK:

json
{
  "message": "Email verified successfully"
}

JWT Token Flow

Arcanflows uses a dual-token system:

TokenLifetimePurpose
Access token15 minutesAuthenticate API requests
Refresh token7 daysObtain new access tokens

Flow

  1. Login via POST /auth/login to receive both tokens
  2. Use the access token in the Authorization: Bearer <token> header for all API calls
  3. When the access token expires (HTTP 401), call POST /auth/refresh with the refresh token
  4. Store tokens securely -- never expose them in client-side code or URLs
  5. On logout, call POST /auth/logout to invalidate both tokens
javascript
// Example: auto-refresh on 401
async function apiCall(url, options = {}) {
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': \`Bearer \${accessToken}\`,
    },
  });

  if (response.status === 401) {
    const tokens = await fetch('/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refresh_token: refreshToken }),
    }).then(r => r.json());

    accessToken = tokens.access_token;
    refreshToken = tokens.refresh_token;

    response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': \`Bearer \${accessToken}\`,
      },
    });
  }

  return response;
}

Multi-Factor Authentication (MFA)

TOTP-based MFA adds an extra layer of security. All MFA management endpoints require authentication.

GET /api/v1/mfa/status

Check whether MFA is enabled for the current user.

bash
curl -X GET /api/v1/mfa/status \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "enabled": true,
  "method": "totp",
  "devices_count": 1,
  "recovery_codes_remaining": 8
}

POST /api/v1/mfa/setup

Begin MFA setup. Returns a QR code and secret for the authenticator app.

bash
curl -X POST /api/v1/mfa/setup \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "secret": "JBSWY3DPEHPK3PXP",
  "qr_code_url": "data:image/png;base64,iVBOR...",
  "otpauth_url": "otpauth://totp/Arcanflows:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Arcanflows",
  "recovery_codes": [
    "A1B2C3D4E5",
    "F6G7H8I9J0",
    "K1L2M3N4O5",
    "P6Q7R8S9T0",
    "U1V2W3X4Y5",
    "Z6A7B8C9D0",
    "E1F2G3H4I5",
    "J6K7L8M9N0",
    "O1P2Q3R4S5",
    "T6U7V8W9X0"
  ]
}

POST /api/v1/mfa/enable

Enable MFA after verifying a TOTP code from the authenticator app. Must call /mfa/setup first.

bash
curl -X POST /api/v1/mfa/enable \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "code": "482910"
  }'

Response 200 OK:

json
{
  "message": "MFA enabled successfully",
  "recovery_codes_count": 10
}

POST /api/v1/mfa/disable

Disable MFA for the current user. Requires the current TOTP code or a recovery code.

bash
curl -X POST /api/v1/mfa/disable \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "code": "193847"
  }'

Response 200 OK:

json
{
  "message": "MFA disabled successfully"
}

POST /api/v1/mfa/verify

Verify a TOTP code (general-purpose verification for sensitive actions).

bash
curl -X POST /api/v1/mfa/verify \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "code": "571930"
  }'

Response 200 OK:

json
{
  "valid": true
}

GET /api/v1/mfa/devices

List registered MFA devices.

bash
curl -X GET /api/v1/mfa/devices \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "devices": [
    {
      "id": "dev_abc123",
      "name": "Google Authenticator",
      "type": "totp",
      "created_at": "2025-08-10T14:30:00Z",
      "last_used_at": "2025-10-05T09:15:00Z"
    }
  ]
}

DELETE /api/v1/mfa/devices/:deviceId

Remove a specific MFA device.

bash
curl -X DELETE /api/v1/mfa/devices/dev_abc123 \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "message": "Device removed successfully"
}

POST /api/v1/mfa/recovery-codes

Generate a new set of recovery codes. Invalidates all previous codes.

bash
curl -X POST /api/v1/mfa/recovery-codes \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "recovery_codes": [
    "A1B2C3D4E5",
    "F6G7H8I9J0",
    "K1L2M3N4O5",
    "P6Q7R8S9T0",
    "U1V2W3X4Y5",
    "Z6A7B8C9D0",
    "E1F2G3H4I5",
    "J6K7L8M9N0",
    "O1P2Q3R4S5",
    "T6U7V8W9X0"
  ],
  "message": "New recovery codes generated. Previous codes are now invalid."
}

MFA Setup Flow

  1. Call POST /api/v1/mfa/setup to get the QR code and secret
  2. Scan the QR code with an authenticator app (Google Authenticator, Authy, etc.)
  3. Enter the 6-digit code from the app into POST /api/v1/mfa/enable
  4. Save the recovery codes in a secure location
  5. Future logins will require MFA -- after POST /auth/login returns mfa_required: true, call POST /auth/mfa/verify with the TOTP code

Programmatic access — API keys

There is no separate long-lived "platform API key" for the management API — to manage resources you authenticate with the JWT access token from /auth/login (above). Two different things are called "API keys"; don't confuse them:

Embed API Key (emb_)Provider API Key
PurposeAuthenticate your calls to a specific agentStore your OpenAI/Anthropic/etc. credentials so the platform can call the LLM
WhereAgent -> Embed Settings (/agents/{id}/embed)Settings -> API Keys / LLM Backends
Endpoint/api/v1/embed/api-keys/api/v1/api-keys
Formatemb_...the provider's own key (sk-...)
Used as request auth?Yes -- for /public/agents/{id}/chatNever

Embed API Keys -- call an agent from your code

An Embed API Key authenticates calls to one specific agent at the public chat endpoint. Create one on the agent's Embed Settings tab, or programmatically with your console JWT:

bash
curl -X POST /api/v1/embed/api-keys \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "AGENT_ID", "name": "Production backend", "allowed_domains": ["*"]}'

Response 201 Created -- the full key is shown only once:

json
{
  "id": "...",
  "agent_id": "AGENT_ID",
  "name": "Production backend",
  "key_prefix": "emb_1a2b",
  "api_key": "emb_1a2b3c4d...full-key-shown-once..."
}

Then call the agent (see the Agents API):

bash
curl -X POST /api/v1/public/agents/AGENT_ID/chat \
  -H "Authorization: Bearer emb_1a2b3c4d..." \
  -H "Content-Type: application/json" \
  -d '{"session_id": "user-42", "message": "Hello!"}'

Each key is scoped to its agent; if allowed_domains is set, requests must come from a listed Origin. The key can also be sent as X-API-Key: emb_... or ?api_key=emb_.... List and revoke keys via GET / DELETE /api/v1/embed/api-keys.

Provider API Keys (/api/v1/api-keys) are unrelated: they hold your LLM provider credentials (sk-...) so the platform can run the models -- they are never used to authenticate API requests.


User Administration

Admin endpoints for managing users. Requires super_admin or tenant_admin role.

GET /users

List all users in the tenant.

bash
curl -X GET /users?page=1&per_page=20 \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "users": [
    {
      "id": "usr_abc123",
      "email": "[email protected]",
      "first_name": "Jane",
      "last_name": "Doe",
      "role": "agent_developer",
      "status": "active",
      "mfa_enabled": true,
      "email_verified": true,
      "last_login_at": "2025-10-05T09:15:00Z",
      "created_at": "2025-08-10T14:30:00Z"
    }
  ],
  "total": 42,
  "page": 1,
  "per_page": 20
}

PUT /users/:id/role

Update a user's role.

bash
curl -X PUT /users/usr_abc123/role \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "role": "agent_developer"
  }'

Valid roles: super_admin, tenant_admin, agent_developer, agent_user, viewer

Response 200 OK:

json
{
  "id": "usr_abc123",
  "role": "agent_developer",
  "message": "Role updated successfully"
}

DELETE /users/:id

Permanently delete a user account.

bash
curl -X DELETE /users/usr_abc123 \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "message": "User deleted successfully"
}

POST /users/:id/deactivate

Deactivate a user account. The user will not be able to log in.

bash
curl -X POST /users/usr_abc123/deactivate \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "id": "usr_abc123",
  "status": "inactive",
  "message": "User deactivated successfully"
}

POST /users/:id/reactivate

Reactivate a previously deactivated user account.

bash
curl -X POST /users/usr_abc123/reactivate \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "id": "usr_abc123",
  "status": "active",
  "message": "User reactivated successfully"
}

POST /api/v1/users/:id/mfa/reset

Reset MFA for a specific user (admin only). This disables MFA on the user's account so they can set it up again.

bash
curl -X POST /api/v1/users/usr_abc123/mfa/reset \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "id": "usr_abc123",
  "mfa_enabled": false,
  "message": "MFA reset successfully. User must set up MFA again."
}

Profile

Manage the authenticated user's profile and avatar.

GET /api/v1/profile

Get the current user's profile.

bash
curl -X GET /api/v1/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "id": "usr_abc123",
  "email": "[email protected]",
  "first_name": "Jane",
  "last_name": "Doe",
  "avatar_url": "https://cdn.arcanflows.io/avatars/usr_abc123.jpg",
  "timezone": "America/New_York",
  "language": "en",
  "role": "agent_developer",
  "tenant_id": "tnt_xyz789",
  "created_at": "2025-08-10T14:30:00Z",
  "updated_at": "2025-10-05T09:15:00Z"
}

PUT /api/v1/profile

Update profile information.

bash
curl -X PUT /api/v1/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Jane",
    "last_name": "Smith",
    "timezone": "America/Chicago",
    "language": "en"
  }'

Response 200 OK:

json
{
  "id": "usr_abc123",
  "first_name": "Jane",
  "last_name": "Smith",
  "timezone": "America/Chicago",
  "language": "en",
  "updated_at": "2025-10-06T11:00:00Z"
}

POST /api/v1/profile/avatar

Upload a profile avatar. Send as multipart/form-data.

bash
curl -X POST /api/v1/profile/avatar \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -F "avatar=@/path/to/photo.jpg"

Response 200 OK:

json
{
  "avatar_url": "https://cdn.arcanflows.io/avatars/usr_abc123.jpg",
  "message": "Avatar uploaded successfully"
}

DELETE /api/v1/profile/avatar

Remove the current avatar.

bash
curl -X DELETE /api/v1/profile/avatar \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "message": "Avatar removed successfully"
}

Permissions

Manage role-based access control. Permission endpoints allow viewing and updating what each role can do.

GET /me/permissions

Get permissions for the current user based on their role.

bash
curl -X GET /me/permissions \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "role": "agent_developer",
  "permissions": [
    "agents:read",
    "agents:write",
    "agents:delete",
    "agents:chat",
    "workflows:read",
    "workflows:write",
    "workflows:execute",
    "forms:read",
    "forms:write",
    "tools:read",
    "tools:write",
    "conversations:read",
    "conversations:write",
    "data_tables:read",
    "data_tables:write"
  ]
}

GET /permissions

List all available permissions in the system. Requires super_admin or tenant_admin role.

bash
curl -X GET /permissions \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "permissions": [
    {
      "key": "agents:read",
      "description": "List and view agents",
      "category": "agents"
    },
    {
      "key": "agents:write",
      "description": "Create and update agents",
      "category": "agents"
    },
    {
      "key": "admin:users",
      "description": "Manage users and roles",
      "category": "admin"
    }
  ]
}

GET /roles/:role/permissions

Get permissions assigned to a specific role.

bash
curl -X GET /roles/agent_developer/permissions \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response 200 OK:

json
{
  "role": "agent_developer",
  "permissions": [
    "agents:read",
    "agents:write",
    "agents:delete",
    "agents:chat",
    "workflows:read",
    "workflows:write",
    "workflows:execute",
    "forms:read",
    "forms:write",
    "tools:read",
    "tools:write"
  ]
}

PUT /roles/:role/permissions

Update permissions for a role. Requires super_admin.

bash
curl -X PUT /roles/agent_user/permissions \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "permissions": [
      "agents:read",
      "agents:chat",
      "conversations:read",
      "forms:read",
      "forms:submit"
    ]
  }'

Response 200 OK:

json
{
  "role": "agent_user",
  "permissions": [
    "agents:read",
    "agents:chat",
    "conversations:read",
    "forms:read",
    "forms:submit"
  ],
  "message": "Permissions updated successfully"
}

Roles Reference

RoleDescription
super_adminFull platform access across all tenants
tenant_adminFull access within their tenant
agent_developerCreate and manage agents, workflows, tools, and forms
agent_userChat with agents, submit forms, view content
viewerRead-only access to permitted resources

Error Responses

401 Unauthorized

json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or expired token"
  }
}

403 Forbidden

json
{
  "error": {
    "code": "forbidden",
    "message": "Insufficient permissions for this action",
    "details": {
      "required": "agents:write",
      "your_role": "viewer"
    }
  }
}

422 Validation Error

json
{
  "error": {
    "code": "validation_error",
    "message": "Invalid request body",
    "details": {
      "password": "Must be at least 8 characters with one uppercase, one lowercase, and one number"
    }
  }
}

429 Too Many Requests

json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many login attempts. Try again in 300 seconds.",
    "details": {
      "retry_after": 300
    }
  }
}