Skip to main content

Authentication

FeatureSignals uses JWT tokens for management operations and API keys for SDK evaluation.

Register

Create a new account, organization, and default project.

POST /v1/auth/register

Request

{
"email": "admin@example.com",
"password": "securepassword",
"name": "Admin User",
"org_name": "My Company"
}
FieldTypeRequiredDescription
emailstringYesUser email address (corporate or personal)
passwordstringYesMinimum 8 characters with at least 1 uppercase letter, 1 lowercase letter, 1 digit, and 1 special character
namestringYesDisplay name
org_namestringYesOrganization name

Response 201 Created

{
"user": {
"id": "uuid",
"email": "admin@example.com",
"name": "Admin User",
"email_verified": false,
"created_at": "2026-04-01T00:00:00Z"
},
"organization": {
"id": "uuid",
"name": "My Company",
"slug": "my-company",
"created_at": "2026-04-01T00:00:00Z",
"updated_at": "2026-04-01T00:00:00Z"
},
"tokens": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"expires_at": 1711929600
}
}

Registration automatically creates:

  • User with owner role
  • Organization with slug derived from name
  • A default project with three environments: dev, staging, production
  • A verification email is sent immediately

:::tip Recommended For new signups, use the verify-first OTP flow (POST /v1/auth/initiate-signup + POST /v1/auth/complete-signup) documented in Signup & Trial. The legacy register endpoint above does not require email verification before account creation. :::


Login

POST /v1/auth/login

Request

{
"email": "admin@example.com",
"password": "securepassword"
}

Response 200 OK

{
"user": {
"id": "uuid",
"email": "admin@example.com",
"name": "Admin User",
"email_verified": false,
"created_at": "2026-04-01T00:00:00Z"
},
"tokens": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"expires_at": 1711929600
}
}

Error 401 Unauthorized

{"error": "invalid credentials"}

Refresh Token

Exchange a refresh token for a new token pair.

POST /v1/auth/refresh

Request

{
"refresh_token": "eyJ..."
}

Response 200 OK

{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"expires_at": 1711929600
}

Important: Refresh tokens cannot be used in place of access tokens. The server validates token type and will reject refresh tokens presented as Bearer tokens.


Token Configuration

SettingDefaultEnvironment Variable
Access token TTL60 minutesTOKEN_TTL_MINUTES
Refresh token TTL7 daysREFRESH_TTL_HOURS
JWT secretdev-secret-change-in-productionJWT_SECRET

:::danger Production Always set a strong JWT_SECRET in production. The default is insecure. :::


Using Tokens

Include the access token in the Authorization header for management API calls:

curl https://api.featuresignals.com/v1/projects \
-H "Authorization: Bearer eyJ..."

For SDK/evaluation endpoints, use an API key:

curl -X POST https://api.featuresignals.com/v1/evaluate \
-H "X-API-Key: fs_srv_..." \
-H "Content-Type: application/json" \
-d '{"flag_key": "my-flag", "context": {"key": "user-1"}}'

Send Verification Email

Send a verification link to the authenticated user's email address.

POST /v1/auth/send-verification-email

Authentication: Bearer JWT

Response 200 OK

{
"message": "Verification email sent"
}

Verify Email

Verify an email address via the link sent by send-verification-email.

GET /v1/auth/verify-email?token=<token>&email=<email>

Authentication: None (public)

Response

Redirects to the dashboard login page with ?verified=true on success.


Token Exchange

Exchange a one-time token for a full JWT pair. Used during cross-domain authentication (e.g., after PayU payment redirect).

POST /v1/auth/token-exchange

Authentication: None (public — the one-time token serves as authentication)

Request

{
"token": "hex_one_time_token"
}

Response 200 OK

{
"user": {
"id": "uuid",
"email": "jane@company.com",
"name": "Jane Smith",
"email_verified": false,
"created_at": "2026-04-01T00:00:00Z"
},
"tokens": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"expires_at": 1711929600
}
}

One-time tokens are single-use and expire after 5 minutes. A consumed or expired token returns 401 Unauthorized.


Logout

Revoke the current session's JWT. The token is immediately invalidated server-side.

POST /v1/auth/logout

Authentication: Bearer JWT

Response 200 OK

{
"message": "logged out"
}

Multi-Factor Authentication (MFA)

TOTP-based multi-factor authentication (RFC 6238). Compatible with Google Authenticator, Authy, and similar apps. Requires Pro plan or higher.

Enable MFA

Generate a TOTP secret. MFA is not active until verified.

POST /v1/auth/mfa/enable

Authentication: Bearer JWT

Response 200 OK

{
"secret": "BASE32SECRET",
"qr_uri": "otpauth://totp/FeatureSignals:user@example.com?secret=BASE32SECRET&issuer=FeatureSignals"
}

Verify MFA

Activate MFA by providing a valid TOTP code.

POST /v1/auth/mfa/verify

Request

{
"code": "123456"
}

Response 200 OK

{
"message": "MFA enabled"
}

Disable MFA

Deactivate MFA. Requires current password confirmation.

POST /v1/auth/mfa/disable

Request

{
"password": "currentpassword"
}

MFA Status

Check whether MFA is enabled for the authenticated user.

GET /v1/auth/mfa/status

Response 200 OK

{
"enabled": true
}

Login with MFA

When MFA is enabled, include the mfa_code field in the login request:

{
"email": "user@example.com",
"password": "securepassword",
"mfa_code": "123456"
}

If MFA is enabled and mfa_code is missing, the server returns 403 with {"error": "mfa_required"}.


Brute-Force Protection

After 10 consecutive failed login attempts within 15 minutes, the account is temporarily locked. The server returns 429 Too Many Requests:

{
"error": "too many failed login attempts, please try again later"
}