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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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).
bashcurl -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.
bashcurl -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.
bashcurl -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:
| Token | Lifetime | Purpose |
|---|---|---|
| Access token | 15 minutes | Authenticate API requests |
| Refresh token | 7 days | Obtain new access tokens |
Flow
- Login via
POST /auth/loginto receive both tokens - Use the access token in the
Authorization: Bearer <token>header for all API calls - When the access token expires (HTTP 401), call
POST /auth/refreshwith the refresh token - Store tokens securely -- never expose them in client-side code or URLs
- On logout, call
POST /auth/logoutto 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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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).
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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
- Call
POST /api/v1/mfa/setupto get the QR code and secret - Scan the QR code with an authenticator app (Google Authenticator, Authy, etc.)
- Enter the 6-digit code from the app into
POST /api/v1/mfa/enable - Save the recovery codes in a secure location
- Future logins will require MFA -- after
POST /auth/loginreturnsmfa_required: true, callPOST /auth/mfa/verifywith 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 | |
|---|---|---|
| Purpose | Authenticate your calls to a specific agent | Store your OpenAI/Anthropic/etc. credentials so the platform can call the LLM |
| Where | Agent -> Embed Settings (/agents/{id}/embed) | Settings -> API Keys / LLM Backends |
| Endpoint | /api/v1/embed/api-keys | /api/v1/api-keys |
| Format | emb_... | the provider's own key (sk-...) |
| Used as request auth? | Yes -- for /public/agents/{id}/chat | Never |
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:
bashcurl -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):
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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.
bashcurl -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
| Role | Description |
|---|---|
super_admin | Full platform access across all tenants |
tenant_admin | Full access within their tenant |
agent_developer | Create and manage agents, workflows, tools, and forms |
agent_user | Chat with agents, submit forms, view content |
viewer | Read-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 } } }