Auth Endpoints
Complete reference for authentication endpoints.
Complete reference for authentication endpoints.
Public Endpoints
Require: Anon Key
Sign Up
POST /auth/signup
Authorization: Bearer <anon_key>
Content-Type: application/jsonRequest:
{
"email": "user@example.com",
"password": "password123",
"user_metadata": {
"name": "John Doe"
}
}Response: 201 Created
{
"confirmation_required": true,
"message": "If the account was created, a confirmation email has been sent. Confirm your email, then sign in."
}Signup is session-less. Call POST /auth/signin after confirmation to create
a session. Existing emails receive the same acknowledgement.
Errors:
400- Invalid email/password format401- Invalid, tampered, revoked, or wrong-project anon key403- Signups disabled, email domain not inallowed_email_domains, or anon key lacksauth.signuppermission.anonymous.volcano.internalis reserved for anonymous accounts and is always refused429- Rate limit exceeded
Sign In
POST /auth/signin
Authorization: Bearer <anon_key>Request:
{
"email": "user@example.com",
"password": "password123"
}Response: 200 OK
The response contains an access token, refresh token, and user unless the request uses eligible cookie session storage.
Errors:
400- Missing email/password401- Invalid credentials, invalid/tampered/revoked anon key, or account banned/deleted403- Anon key lacksauth.signinpermission, or the email domain is not inallowed_email_domainswhileallowed_email_domains_modeissignup_and_signin. The domain is taken from the account's canonical email — its primary identity — which is not necessarily the address you sign in with429- Rate limit exceeded
Refresh Token
POST /auth/refresh
Authorization: Bearer <anon_key>Request:
{
"refresh_token": "f4e3d2c1..."
}Response: 200 OK
{
"access_token": "eyJhbGc...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "f4e3d2c1...",
"user": {
"id": "uuid",
"email": "user@example.com"
}
}Errors:
401- Invalid/expired refresh token, session timeout, invalid/tampered/revoked anon key403- Anon key lacksauth.refreshpermission, or the email domain is not inallowed_email_domainswhileallowed_email_domains_modeissignup_and_signin
Logout
POST /auth/logout
Authorization: Bearer <anon_key>Request:
{
"refresh_token": "f4e3d2c1..."
}Response: 204 No Content
Use HttpOnly cookie sessions
Use cookie mode when your browser app and the Volcano API share a schemeful
site, such as app.example.com and api.example.com. Configure the app's
exact origin in auth CORS, enable credentials, and send browser requests with
credentials:
const response = await fetch('https://api.example.com/auth/signin', {
method: 'POST',
credentials: 'include',
headers: {
Authorization: `Bearer ${anonKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'user@example.com',
password: 'correct-horse-battery-staple',
session_mode: 'cookie',
}),
});
const session = await response.json();The API stores the refresh token in a project-scoped HttpOnly cookie and omits
it from the response. Refresh and logout with credentials: 'include' and
{"session_mode":"cookie"}; logout returns 204 even when the cookie is
missing or expired.
Wildcard CORS entries and cross-site origins do not qualify for cookie mode.
Those clients continue to receive and send refresh_token in JSON.
A frontend served on its default Volcano URL is one of those cross-site
origins. Frontends answer on volcano.run and the API on volcano.dev, so a
browser would not send the cookie back and the API does not set one. Use the
default JSON token mode there, or serve the app from a custom domain that is
same-site with the API host you call.
Confirm Email
POST /auth/confirm
Authorization: Bearer <anon_key>
Content-Type: application/jsonRequest:
{
"token": "confirmation-token-from-email"
}Response: 200 OK
{
"message": "Email confirmed successfully"
}Also possible (200 OK):
{
"message": "Email already confirmed"
}Confirms user's email address using token from confirmation email.
Errors:
400- Missing confirmation token in request body401- Invalid/expired token or missing anon key403- CORS blocked
Resend Confirmation
POST /auth/resend-confirmation
Authorization: Bearer <anon_key>
Content-Type: application/jsonRequest:
{
"email": "user@example.com"
}Response: 200 OK
{
"message": "If the email exists and is unconfirmed, a confirmation link has been sent"
}Note: Response is generic to prevent email enumeration. If the user exists and is unconfirmed, a new token replaces the previous token (old confirmation links become invalid). If the user does not exist or is already confirmed, no email is sent.
Errors:
401- Missing/invalid anon key403- CORS blocked
Forgot Password
POST /auth/forgot-password
Authorization: Bearer <anon_key>
Content-Type: application/jsonRequest:
{
"email": "user@example.com"
}Response: 200 OK
{
"message": "If the email exists, a password reset link has been sent"
}Note: Response is always the same to prevent email enumeration.
Errors:
401- Missing/invalid anon key403- Password reset disabled or CORS blocked
Reset Password
POST /auth/reset-password
Authorization: Bearer <anon_key>
Content-Type: application/jsonRequest:
{
"token": "recovery-token-from-email",
"new_password": "NewPassword123"
}Response: 200 OK
{
"message": "Password reset successful. Please sign in with your new password."
}Effects:
- Password updated
- Recovery token cleared
- All sessions revoked
Errors:
400- Password doesn't meet requirements or in password history401- Invalid/expired token403- Password reset disabled
Sign Up Anonymous
POST /auth/signup-anonymous
Authorization: Bearer <anon_key>
Content-Type: application/jsonResponse: 201 Created
{
"access_token": "eyJhbGc...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "f4e3d2c1...",
"user": {
"id": "uuid",
"email": "anon-xyz@anonymous.volcano.internal",
"user_metadata": {"anonymous": true},
"status": "active"
}
}Creates guest user without email/password.
Errors:
401- Missing/invalid anon key403- Anonymous signins disabled or CORS blocked
User Endpoints
Require: Access Token
Get Current User
GET /auth/user
Authorization: Bearer <access_token>Response: 200 OK
{
"user": {
"id": "uuid",
"email": "user@example.com",
"email_confirmed": false,
"user_metadata": {...},
"status": "active",
"created_at": "...",
"last_sign_in_at": "..."
}
}Update User
PUT /auth/user
Authorization: Bearer <access_token>Request:
{
"password": "newpassword123",
"user_metadata": {
"name": "Jane Doe"
}
}Response: 200 OK (updated user object)
user_metadata is merged by key. Existing keys that are not included in the
request remain unchanged. Set a key to null to remove it.
Merging is shallow: a nested object replaces the stored value for that top-level key.
Errors:
400- Password doesn't meet requirements or in password history
Convert Anonymous User
POST /auth/user/convert-anonymous
Authorization: Bearer <access_token>
Content-Type: application/jsonRequest:
{
"email": "user@example.com",
"password": "Password123",
"user_metadata": {
"name": "John Doe"
}
}Response: 200 OK
{
"user": {
"id": "uuid",
"email": "user@example.com",
"user_metadata": {...},
"status": "active"
}
}Converts anonymous user to authenticated user. User ID is preserved.
If require_email_confirmation=true, the converted user must confirm email
before email/password signin succeeds. When email sending is enabled, conversion
triggers a confirmation email.
Errors:
400- User is already authenticated (not anonymous)403- Email domain not inallowed_email_domains409- Email already in use
Get My Sessions
GET /auth/user/sessions
Authorization: Bearer <access_token>Response: 200 OK
{
"sessions": [
{
"id": "session-uuid",
"provider": "email",
"user_agent": "Mozilla/5.0...",
"ip_address": "192.168.1.1",
"last_ip_address": "10.0.0.50",
"is_active": true,
"is_current": true,
"created_at": "2024-01-14T08:00:00Z"
}
],
"total": 1
}Returns the current user's sessions. The is_current field indicates which session is making the current request.
Ordering and pagination. By default sessions come back in most-recently-active
order, paged with page and limit (default 20, max 100), in the body shown
above. Add sort=created_at to order by session start instead and unlock the
standard cursor pagination used by the other list endpoints:
GET /auth/user/sessions?sort=created_at&limit=20
GET /auth/user/sessions?sort=created_at&limit=20&cursor=<next_cursor>
GET /auth/user/sessions?sort=created_at&limit=20&ending_before=<prev_cursor>&offset=40Cursor responses use the shared envelope: data, limit, total, has_more,
next_cursor, prev_cursor.
Cursor pagination requires sort=created_at and returns 400 otherwise. Activity
order cannot be paged by cursor: last_activity_at is rewritten whenever a
session refreshes its token, so a session that becomes active mid-paging moves
above the cursor and would never appear in the remaining pages.
Add status=active to hide expired sessions, or status=expired to show only
those. Both filters also narrow total. The expired-only filter uses offset
pagination: cursor mode rejects it because a session can expire above the
cursor anchor during a walk. Cursor offsets may not exceed 100,000 rows.
Delete My Session
DELETE /auth/user/sessions/{sessionId}
Authorization: Bearer <access_token>Response: 204 No Content
Signs out from a specific device/session.
Errors:
404- Session not found or doesn't belong to user
Delete All Other Sessions
DELETE /auth/user/sessions
Authorization: Bearer <access_token>Response: 204 No Content
Signs out from all devices except the current session. Useful for "sign out everywhere else" functionality.
Admin Endpoints
Require: Platform User Token
List Auth Users
GET /auth/users?page=1&limit=10
Authorization: Bearer <platform_token>Response: 200 OK
{
"data": [...],
"page": 1,
"limit": 10,
"total": 25,
"has_more": true
}Get Auth User
GET /auth/users/{userId}
Authorization: Bearer <platform_token>Delete Auth User
DELETE /auth/users/{userId}
Authorization: Bearer <platform_token>Response: 204 No Content
Effect: Soft delete (status='deleted'), all sessions revoked
List User Sessions
GET /auth/users/{userId}/sessions
Authorization: Bearer <platform_token>Response: 200 OK
{
"sessions": [
{
"id": "session-uuid",
"user_id": "user-uuid",
"provider": "email",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
"ip_address": "192.168.1.1",
"expires_at": "2024-01-15T12:00:00Z",
"last_activity_at": "2024-01-14T10:30:00Z",
"session_started_at": "2024-01-14T08:00:00Z",
"is_active": true,
"created_at": "2024-01-14T08:00:00Z",
"updated_at": "2024-01-14T10:30:00Z"
}
],
"total": 1
}Fields:
provider: Authentication method (email,google,github,facebook,apple,anonymous)ip_address: Client IP that created the session
Delete User Session
Revoke a specific session (log out from one device):
DELETE /auth/users/{userId}/sessions/{sessionId}
Authorization: Bearer <platform_token>Response: 204 No Content
Delete All User Sessions
Revoke all sessions (log out from everywhere):
DELETE /auth/users/{userId}/sessions
Authorization: Bearer <platform_token>Response: 204 No Content
Use cases:
- User reports compromised account
- Force password change
- Security incident response
Anon Key Endpoints
Require: Platform User Token
List Anon Keys
GET /projects/{projectId}/anon-keys
Authorization: Bearer <platform_token>Create Anon Key
POST /projects/{projectId}/anon-keys
Authorization: Bearer <platform_token>Request:
{"name": "production-key"}Response: 201 Created (includes key_value JWT)
Delete Anon Key
DELETE /projects/{projectId}/anon-keys/{keyId}
Authorization: Bearer <platform_token>Response: 204 No Content
Permanently deletes the anon key. Any clients using this key will immediately lose access.
Regenerate Anon Key
POST /projects/{projectId}/anon-keys/{keyId}/regenerate
Authorization: Bearer <platform_token>Response: 200 OK (new key_value)
Configuration Endpoints
Get Config
GET /projects/{projectId}/auth/config
Authorization: Bearer <platform_token>Update Config
PUT /projects/{projectId}/auth/config
Authorization: Bearer <platform_token>Request: (all fields optional)
{
"access_token_lifetime": 7200,
"min_password_length": 20,
"require_numbers": true,
"email_enabled": true,
"require_email_confirmation": true,
"cors_enabled": true,
"cors_allowed_origins": ["https://myapp.com"],
"allowed_email_domains": ["domain1.com", "domain2.com"],
"allowed_email_domains_mode": "signup",
"device_verification_url": "https://myapp.com/device"
}Validation rules:
min_password_lengthmust be between 15 and 128 Unicode charactersrequire_email_confirmation=truerequiresemail_enabled=trueemail_enabled=falseis rejected whilerequire_email_confirmation=truepost_auth_redirect_urlandpost_logout_redirect_urlmust be present inallowed_redirect_urlsallowed_email_domainsentries must be bare domains (domain1.com, notuser@domain1.comorlocalhost), max 100. They are stored lowercased and replace the previous list;[]removes the restriction. The list is enforced on SUPERAGENT only — a downgrade parks it without discarding it. See Email Domain Allowlist.allowed_email_domains_modemust bedisabled,signup(default), orsignup_and_signin.signup_and_signinalso blocks sign-in for accounts outside the list and deletes their sessions when the config is saved.device_verification_url, when set, must be a valid http/https URL. It overrides where device-code (CLI) logins send users to approve; theuser_codeis appended automatically. Unlike the redirect URLs it is not tied toallowed_redirect_urls. Send an empty string to clear it and fall back to the managed device page. When a custom URL is set, device login does not requiremanaged_auth_enabled(but the custom page's origin must be incors_allowed_originsto call/auth/device/verify).
Errors:
403- The update would set or widenallowed_email_domainson a project that is not on the SUPERAGENT plan. Removing the restriction stays available on every plan
Send Test Email
Sends a diagnostic email using the project's saved SMTP config so you can
verify your setup works. Save your config first — the endpoint reads the
persisted auth_config, not request-time settings.
POST /projects/{projectId}/auth/config/test-email
Authorization: Bearer <platform_token>Request:
{
"to_email": "you@yourdomain.com"
}Supplying html_body and/or text_body (with an optional subject) instead
renders a custom message — used by the template editor's "Send Test" preview.
Sending subject alone (no body) is rejected.
Responses:
200—{"success": true}on delivery400— invalidto_email, oremail_enabled=false/smtp_hostempty, orsubjectwithout a body403— caller is not the project owner404— project not found502— template render failure or SMTP delivery failed (message includes the SMTP-level reason)
Managed Hosted Auth Pages
Admin endpoints (platform token required):
GET /projects/{projectId}/auth/hosted-pages/{pageType}
PUT /projects/{projectId}/auth/hosted-pages/{pageType}Supported pageType values:
loginreset-password
Public render endpoint (no auth token):
GET /projects/{projectId}/auth/hosted
GET /projects/{projectId}/auth/hosted/reset-passwordOptional unified deep links:
GET /projects/{projectId}/auth/hosted?action=login
GET /projects/{projectId}/auth/hosted?action=signup
GET /projects/{projectId}/auth/hosted?action=forgot-password
GET /projects/{projectId}/auth/hosted?action=device&user_code=ABCD-EFGH&anon_key=<anon_key>Smart login helpers (public):
GET /projects/{projectId}/auth/hosted/login/options?anon_key=<anon_key>
POST /projects/{projectId}/auth/hosted/login/check-email
Authorization: Bearer <anon_key>Rate limiting:
- Both helper endpoints are rate limited per project and client IP.
- On abuse, they apply exponential backoff and then return
429withRetry-After.
Behavior notes:
- Render requires
Accept: text/html. - Latest saved page content is rendered.
- Managed pages render only when
managed_auth_enabled=true. GET /projects/{projectId}/auth/hostedis the unified login/signup/forgot-password entrypoint.GET /projects/{projectId}/auth/hosted?action=device&user_code=...&anon_key=...renders managed device authorization approval UX.GET /projects/{projectId}/auth/hosted/reset-passwordis the dedicated reset-password page.- Redirect users from your app to
GET /projects/{projectId}/auth/hosted?anon_key=<anon_key>to use managed hosted auth. - HTML validation rejects
<script>,javascript:URLs, inline event handlers, andiframe/object/embed/meta/linktags. - CSS validation rejects closing
</styleand</headtag patterns. - HTML and CSS are each limited to 256 KiB.
- Public render responses include strict CSP,
X-Frame-Options: DENY,X-Content-Type-Options: nosniff, andCache-Control: no-store.
See Also
- Authentication - Auth headers and token types
- Errors - Error handling