Skip to main content

Authentication

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

Signup (OTP-Verified)​

New user registration is a 2-step process that verifies email ownership before creating any permanent records. A 6-digit OTP is sent to the user's email.

Step 1 — Initiate Signup​

POST /v1/auth/initiate-signup

Sends a 6-digit OTP to the user's email via the configured email provider.

Request​

{
"email": "jane@company.com",
"password": "SecurePass123!",
"name": "Jane Smith",
"org_name": "Acme Inc",
"data_region": "us"
}
FieldTypeRequiredDescription
emailstringYesMust be a valid, non-disposable email
passwordstringYesMin 8 chars, 1 upper, 1 lower, 1 digit, 1 special
namestringYesDisplay name (max 255 characters)
org_namestringYesOrganization name (max 255 characters)
data_regionstringNoData residency region. Defaults to us

Response 200 OK​

{
"message": "Verification code sent to your email",
"expires_in": 600
}

Error 409 Conflict​

{"error": "email already registered"}

Step 2 — Complete Signup​

POST /v1/auth/complete-signup

Verifies the OTP and creates the user, organization, and default project atomically.

Request​

{
"email": "jane@company.com",
"otp": "123456"
}

Response 201 Created​

{
"user": {
"id": "uuid",
"email": "jane@company.com",
"name": "Jane Smith",
"email_verified": true,
"created_at": "2026-04-01T00:00:00Z"
},
"organization": {
"id": "uuid",
"name": "Acme Inc",
"slug": "acme-inc",
"plan": "trial",
"trial_expires_at": "2026-04-18T00:00:00Z",
"created_at": "2026-04-01T00:00:00Z",
"updated_at": "2026-04-01T00:00:00Z"
},
"tokens": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"expires_at": 1711929600
},
"onboarding_completed": false
}

Signup automatically creates:

  • User with owner role and verified email
  • Organization with slug derived from name, on a 14-day free trial
  • A default project with three environments: dev, staging, production
  • A welcome email is sent asynchronously

Error 400 Bad Request​

Returned when the OTP is invalid, expired, or no pending signup exists for the email.

Error 429 Too Many Requests​

Returned after too many failed OTP attempts.

Resend OTP​

POST /v1/auth/resend-signup-otp

Request​

{
"email": "jane@company.com"
}

Response 200 OK​

{
"message": "New verification code sent to your email",
"expires_in": 600
}

Rate-limited to one resend every 60 seconds. OTP expires after 10 minutes.


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 Flag Engine 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"
}