Magic Link
Passwordless authentication via email magic links.
Magic Link Authentication
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.
Request Magic Link
/api/v1/auth/magic-link/requestSend a magic link email to the specified address. The link contains a one-time token that can be exchanged for JWT tokens.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Required | The email address to send the magic link to. |
source | string | Optional | Where the magic link was requested from. One of: mobile, web. Affects the format of the generated link. |
allow_registration | boolean | Optional | If true, a new account is created when no account exists for this email. Defaults to false. |
referral_code | string | Optional | Referral code from an existing user. Only used when allow_registration is true and a new account is created. |
terms_accepted | boolean | Optional | Whether 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
{
"success": true,
"data": {
"email": "user@example.com",
"expires_at": "2026-02-08T13:15:00Z",
"is_new_user": false,
"expires_in_minutes": 15
}
}{
"success": false,
"error": {
"code": "USER_NOT_FOUND",
"message": "No account found for this email address."
}
}{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Too many magic link requests. Please wait before trying again.",
"retry_after": 60
}
}Verify Magic Link
/api/v1/auth/magic-link/verifyExchange 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.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Required | The one-time token from the magic link URL. |
device_id | string | Optional | Unique device identifier (UUID recommended). |
platform | string | Optional | Client platform. One of: ios, android, windows, macos, linux, web. |
device_name | string | Optional | Human-readable device name. |
fcm_token | string | Optional | Firebase 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
{
"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"
}
}
}{
"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.
{
"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
/api/v1/auth/2fa/completeFinish 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.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
pending_token | string | Required | The two_factor.pending_token from the requires_2fa response. Valid for 300 seconds and usable for one successful sign-in. |
code | string | Required | The 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
{
"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"
}
}
}{
"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"
}{
"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"
}{
"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"
}{
"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
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.
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.
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.
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.
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.
Related Endpoints
- Login -- Email/password authentication
- Register -- Create account with password
- OAuth Login -- Social provider authentication
- Two-Factor Authentication -- Set up TOTP and recovery codes