Magic Link

Passwordless authentication via email magic links.

Passwordless authentication that sends a one-time login link to the user's email address. The user clicks the link to authenticate without entering a password.

Two-factor authentication applies

A magic link proves access to the mailbox only. If the account has two-factor authentication (TOTP) turned on, verifying the link does not return tokens. It returns a requires_2fa challenge, and the sign-in finishes at POST /api/v1/auth/2fa/complete with the user's 6-digit code or a recovery code.


POST/api/v1/auth/magic-link/request

Send a magic link email to the specified address. The link contains a one-time token that can be exchanged for JWT tokens.

Authentication:No Auth Required

Request Parameters

ParameterTypeRequiredDescription
emailstring RequiredThe email address to send the magic link to.
sourcestringOptionalWhere the magic link was requested from. One of: mobile, web. Affects the format of the generated link.
allow_registrationbooleanOptionalIf true, a new account is created when no account exists for this email. Defaults to false.
referral_codestringOptionalReferral code from an existing user. Only used when allow_registration is true and a new account is created.
terms_acceptedbooleanOptionalWhether the user has accepted the terms of service. Required when allow_registration is true.

Code Examples

curl -X POST https://api.orbai.world/api/v1/auth/magic-link/request \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "source": "web",
    "allow_registration": true,
    "terms_accepted": true
  }'

Responses

200Magic link email sent successfully.
{
  "success": true,
  "data": {
    "email": "user@example.com",
    "expires_at": "2026-02-08T13:15:00Z",
    "is_new_user": false,
    "expires_in_minutes": 15
  }
}
404No account exists for this email and allow_registration is false.
{
  "success": false,
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "No account found for this email address."
  }
}
429Too many magic link requests. Rate limited.
{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many magic link requests. Please wait before trying again.",
    "retry_after": 60
  }
}

POST/api/v1/auth/magic-link/verify

Exchange the magic link token for JWT access and refresh tokens. The token is single-use and expires after the time indicated in the request response. For an account with two-factor authentication, the response is a requires_2fa challenge instead, completed at POST /api/v1/auth/2fa/complete.

Authentication:No Auth Required

Request Parameters

ParameterTypeRequiredDescription
tokenstring RequiredThe one-time token from the magic link URL.
device_idstringOptionalUnique device identifier (UUID recommended).
platformstringOptionalClient platform. One of: ios, android, windows, macos, linux, web.
device_namestringOptionalHuman-readable device name.
fcm_tokenstringOptionalFirebase Cloud Messaging token for push notifications.

Code Examples

curl -X POST https://api.orbai.world/api/v1/auth/magic-link/verify \
  -H "Content-Type: application/json" \
  -d '{
    "token": "mlk_a1b2c3d4e5f6...",
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "platform": "web",
    "device_name": "Chrome on MacBook"
  }'

Responses

200Token verified and the account has no two-factor authentication. Returns the standard AuthResponse with user profile, JWT tokens, and subscription.
{
  "success": true,
  "data": {
    "user": {
      "id": 1,
      "uuid": "usr_abc123",
      "username": "john_doe",
      "email": "user@example.com",
      "role": "USER",
      "active": true
    },
    "tokens": {
      "access_token": "eyJhbGciOiJIUzI1NiIs...",
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2...",
      "access_token_expires_at": "2026-02-09T12:00:00Z",
      "refresh_token_expires_at": "2026-03-08T12:00:00Z",
      "token_type": "Bearer"
    },
    "subscription": {
      "plan_name": "Premium",
      "status": "active",
      "expires_at": "2026-12-31T23:59:59Z"
    }
  }
}
200Token verified, but the account has two-factor authentication. No tokens are issued. The response names the account by email only and carries a short-lived pending token for the completion step.
{
  "success": true,
  "data": {
    "requires_2fa": true,
    "user": {
      "email": "user@example.com"
    },
    "two_factor": {
      "pending_token": "eyJhbGciOiJIUzI1NiIs...",
      "expires_in": 300,
      "methods": ["totp", "recovery_code"]
    }
  }
}

Check requires_2fa before reading tokens

Branch on data.requires_2fa. When it is true, ask the user for the code from their authenticator app (or a recovery code) and send it with pending_token to POST /api/v1/auth/2fa/complete. The magic link stays unused until that step succeeds, so a sign-in abandoned at the code prompt does not burn the link.

401The magic link token is invalid, expired, or has already been used.
{
  "success": false,
  "error": {
    "code": "INVALID_MAGIC_LINK",
    "message": "This magic link is invalid or has expired. Please request a new one."
  }
}

Complete Two-Factor Sign-In

POST/api/v1/auth/2fa/complete

Finish a sign-in that answered requires_2fa. Send the pending token with a TOTP code or a recovery code. On success, the response is the same AuthResponse the original sign-in would have returned.

Authentication:No Auth Required

The same endpoint completes every sign-in method that can answer requires_2fa: magic link, password login, Google and Apple sign-in, passkeys used without user verification, and the Telegram mini-app. The pending token remembers which method started the sign-in, and which device. The session you get back is the one that method would have issued, including the device-limit check.

Request Parameters

ParameterTypeRequiredDescription
pending_tokenstring RequiredThe two_factor.pending_token from the requires_2fa response. Valid for 300 seconds and usable for one successful sign-in.
codestring RequiredThe 6-digit code from the authenticator app, or one of the account's recovery codes (format xxxx-xxxxxx; the hyphen, spaces and letter case are optional). A recovery code works once.

Code Examples

curl -X POST https://api.orbai.world/api/v1/auth/2fa/complete \
  -H "Content-Type: application/json" \
  -d '{
    "pending_token": "eyJhbGciOiJIUzI1NiIs...",
    "code": "123456"
  }'

Responses

200The code is correct. Returns the standard AuthResponse (user, tokens, subscription) for the method that started the sign-in.
{
  "success": true,
  "data": {
    "user": {
      "id": 1,
      "uuid": "usr_abc123",
      "email": "user@example.com",
      "role": "USER",
      "active": true
    },
    "tokens": {
      "access_token": "eyJhbGciOiJIUzI1NiIs...",
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2...",
      "access_token_expires_at": "2026-02-09T12:00:00Z",
      "refresh_token_expires_at": "2026-03-08T12:00:00Z",
      "token_type": "Bearer"
    },
    "subscription": {
      "plan_name": "Premium",
      "status": "active",
      "expires_at": "2026-12-31T23:59:59Z"
    }
  }
}
401The code is wrong. Keep the pending token and ask the user again.
{
  "type": "https://api.orbai.world/errors/TWO_FACTOR_INVALID",
  "title": "Invalid Code",
  "status": 401,
  "detail": "The two-factor code is incorrect",
  "code": "TWO_FACTOR_INVALID"
}
429Too many wrong codes for this account: at most 5 attempts in 10 minutes. Wait, then try again.
{
  "type": "https://api.orbai.world/errors/TWO_FACTOR_LOCKED",
  "title": "Too Many Attempts",
  "status": 429,
  "detail": "Too many incorrect codes. Please wait a few minutes and try again",
  "code": "TWO_FACTOR_LOCKED"
}
401The pending token is expired, already used, forged, or not a pending token; or the magic link was already used by another sign-in. Start the sign-in again.
{
  "type": "https://api.orbai.world/errors/TWO_FACTOR_PENDING_INVALID",
  "title": "Sign-In Expired",
  "status": 401,
  "detail": "This sign-in has expired or was already completed. Please start again",
  "code": "TWO_FACTOR_PENDING_INVALID"
}
403Device sign-in only: the plan's device slots are full. Free a device with device_logout_token, then send the same pending_token again with a valid code.
{
  "error": "DEVICE_LIMIT_EXCEEDED",
  "message": "You have reached your device limit. You are logged in on 2 devices but your plan allows only 2.",
  "current_devices": 2,
  "max_devices": 2,
  "active_devices": [
    {
      "device_id": "550e8400-e29b-41d4-a716-446655440000",
      "device_name": "Pixel 8",
      "platform": "android",
      "login_date": "2026-01-15T09:30:00Z",
      "is_current": false
    }
  ],
  "device_logout_token": "eyJhbGciOiJIUzI1NiIs..."
}

Error bodies use Problem Details

Errors from this endpoint are RFC 7807 Problem Details (application/problem+json). Read the machine-readable code field. The pending token is not an access token: the API refuses it as a bearer on every authenticated route.


Flow Overview

1

Request magic link

Your client calls POST /api/v1/auth/magic-link/request with the user's email. The API sends a magic link to their inbox.

2

User clicks the link

The user opens their email and clicks the magic link. The link redirects to your application with a token parameter in the URL.

3

Verify the token

Your client extracts the token from the URL and calls POST /api/v1/auth/magic-link/verify to exchange it for JWT tokens.

4

Second factor, if the account has one

If the response has requires_2fa set to true, ask for the 6-digit authenticator code (or a recovery code) and send it with two_factor.pending_token to POST /api/v1/auth/2fa/complete. A wrong code returns TWO_FACTOR_INVALID; ask again with the same pending token.

5

User is authenticated

On success, you receive the standard AuthResponse with access and refresh tokens. Store them securely and proceed with the authenticated session.

Token Expiration

Magic link tokens expire after 15 minutes and can only be used once. If the token has expired, the user must request a new magic link. With two-factor authentication, the link is used up when the completion step succeeds. The pending token from the verify step is valid for 5 minutes.

Mobile Deep Links

When source is set to mobile, the magic link uses a deep link URL scheme that opens directly in the mobile app. For web, a standard HTTPS link is used.