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_REQUIRED in clients. Never store or log the mfaToken.

← Getting Started  ·  Messaging →