Authentication
ArelHub exposes two authentication mechanisms for the public API surface:
| Mechanism | Header | When to use |
|---|---|---|
| User JWT | Authorization: Bearer <token> |
User-session flows, dashboards, browser clients |
| API Key | X-Api-Key: <key> |
Server-to-server integrations, automation, CI |
Endpoints that accept either scheme are marked in the reference spec with both
jwt and
api-key security alternatives.
User JWT flow
Login
POST /v1/auth/login
Content-Type: application/json
{
"email": "dev@example.com",
"password": "YourPassword123!"
}
Returns accessToken (short-lived) and refreshToken (long-lived). Store the refresh token
securely — it is a bearer credential.
If the user has MFA enabled, the response is instead 401 MFA_REQUIRED with an
mfaToken. Finish the sign-in with POST /v1/auth/login/mfa (see below).
Refresh
POST /v1/auth/token/refresh
Content-Type: application/json
{
"refreshToken": "<your-refresh-token>"
}
Revoke
POST /v1/auth/token/revoke
Authorization: Bearer eyJ...
Session management
GET /v1/auth/token/sessions
POST /v1/auth/token/sessions/terminate-others
Authorization: Bearer eyJ...
Multi-factor authentication (MFA)
Users can protect their sign-in with an authenticator app (6-digit time-based codes). MFA is optional and enrolled per user. Once enrolled, every way of getting a session for that user needs a second factor: password login, external (OAuth) login and token refresh. API keys are not affected.
Sign-in when MFA is enabled
POST /v1/auth/login and POST /v1/auth/external-login verify the first factor as usual.
If the user has MFA enabled, no tokens or refresh cookie are issued. The API returns 401 with the
MFA_REQUIRED problem code:
{
"type": "https://arelhub.com/errors/mfa-required",
"title": "Multi-factor authentication required",
"status": 401,
"detail": "Enter the code from your authenticator app to finish signing in.",
"code": "MFA_REQUIRED",
"traceId": "00-abc123-def456-00",
"mfaToken": "<opaque, single-use>",
"mfaTokenExpiresAt": "2026-10-05T14:35:00Z",
"mfaMethods": ["totp", "recovery_code"]
}
MFA_REQUIRED is returned only after the password (or external identity) is verified. The
mfaToken is valid for about five minutes, works once and has a small attempt cap. Keep it in memory
only and never log it. Treat this 401 as "ask for a code", not as bad credentials.
Second step
POST /v1/auth/login/mfa
Content-Type: application/json
{
"mfaToken": "<mfaToken from the MFA_REQUIRED response>",
"code": "123456",
"method": "totp",
"rememberMe": false
}
method is totp (the code from the authenticator app) or recovery_code (a
code like XXXXX-XXXXX). Success returns the normal login result, plus
recoveryCodesRemaining when a recovery code was used.
| Status | Code | Meaning |
|---|---|---|
400 |
MFA_CHALLENGE_INVALID |
The mfaToken is missing, expired, used, locked, or the user is no longer active. Sign in
again. |
400 |
INVALID_CODE |
Wrong code. attemptsRemaining shows the tries left for this mfaToken. |
429 |
LOCKED |
Too many wrong codes for this sign-in. |
A refresh token created before the user enabled MFA is rejected after enrolment with 401 MFA_REQUIRED
(without an mfaToken). Treat it as an expired session and sign in again.
Managing MFA
Self-service routes use the user's JWT. Routes marked "step-up" also need an X-Step-Up-Token header.
GET /v1/auth/mfa # status: enabled, method, enrolledAt, recoveryCodesRemaining
POST /v1/auth/mfa/totp/setup # step-up. Returns otpauthUri, manualKey, expiresAt
POST /v1/auth/mfa/totp/confirm # { "code": "123456" }. Enables MFA, returns recovery codes once
POST /v1/auth/mfa/recovery-codes # step-up. { "code": "123456" }. Replaces the recovery codes
DELETE /v1/auth/mfa # step-up. { "code": "...", "method": "totp" }. Disables MFA
Enabling MFA returns 10 single-use recovery codes, shown once. Store them separately from the authenticator device. If a user loses both, support can reset MFA after verifying their identity. A reset never signs the user in.
Passkeys and security keys
A user with MFA on can add passkeys (WebAuthn) as an additional second factor. A passkey never replaces the
authenticator app, and turning MFA off or a staff reset removes the user's passkeys. Passkeys are off until an
environment turns them on. All binary fields are base64url strings, and each options object goes
straight to navigator.credentials.create or .get after decoding.
GET /v1/auth/mfa/webauthn
POST /v1/auth/mfa/webauthn/register/options # step-up MfaManage
POST /v1/auth/mfa/webauthn/register/complete # { challengeId, nickname, credential }
PATCH /v1/auth/mfa/webauthn/{id}
DELETE /v1/auth/mfa/webauthn/{id} # step-up MfaManage
POST /v1/auth/login/mfa/webauthn/options # { mfaToken }
POST /v1/auth/login/mfa # { mfaToken, method: "webauthn", webAuthnChallengeId, assertion }
POST /v1/auth/mfa/step-up/webauthn/options
POST /v1/auth/mfa/step-up # { action, method: "webauthn", webAuthnChallengeId, assertion }
Step-up
A user with MFA on proves step-up with a current authenticator code:
POST /v1/auth/mfa/step-up with { "action": "ApiKey", "code": "123456" }. The response
contains the single-use stepUpToken for the X-Step-Up-Token header. Recovery codes are
not accepted, and repeated wrong codes return 429 LOCKED. A user with MFA on cannot use email OTP for
step-up (403 STEP_UP_FACTOR_REQUIRED), except for MfaManage so a lost authenticator can
still be recovered.
A user with MFA off uses email OTP. Issue an email OTP with POST /v1/email-otp/issue ({ "purpose": "StepUp" }), then verify
it with POST /v1/email-otp/verify and the action you are about to perform. The response contains a
single-use stepUpToken for the X-Step-Up-Token header:
POST /v1/email-otp/verify
Authorization: Bearer eyJ...
Content-Type: application/json
{
"challengeId": "<challengeId from issue>",
"code": "123456",
"action": "MfaManage"
}
API key flow
API keys carry a set of permissions (ApiKeyRights) scoped at creation time.
POST /v1/api-keys
Authorization: Bearer eyJ...
Content-Type: application/json
{
"name": "CI pipeline key",
"rights": ["MessageSend", "MessageRead"]
}
POST /v1/accounts/{accountId}/messages
X-Api-Key: arh_live_...
Content-Type: application/json
{
"subaccountId": "...",
"to": "+15551234567",
"body": "Hello!"
}
Email OTP step-up
POST /v1/email-otp/issue
POST /v1/email-otp/verify
Authorization: Bearer eyJ...
Password management
POST /v1/auth/forgot-password # unauthenticated
POST /v1/auth/reset-password # with reset token from email
POST /v1/auth/change-password # authenticated
Security considerations
- Never embed JWT tokens or API keys in client-side code or version control.
- Refresh tokens are long-lived bearer credentials — treat them with the same care as passwords.
- Prefer API keys with minimum required rights for automation.
- Rotate API keys on suspected compromise via
POST /v1/api-keys/{keyId}/rotate. - Encourage users to enable MFA and to keep recovery codes apart from their authenticator device.
- Handle
401 MFA_REQUIREDin clients. Never store or log themfaToken.