This walks through a full passwordless login with plain curl, without the SeamlessAuth SDKs.
It is the "Direct HTTP APIs (advanced)" path: you call this API directly and take custody of the
returned tokens yourself. For browser apps, the recommended path is a trusted server adapter that
holds token custody for you (see Deployment Topology).
The example uses email OTP because it is the easiest flow to drive from a shell. The same
shape applies to phone OTP (swap email for phone endpoints).
Assumptions:
- The API is running at
http://localhost:5312. email_otpis inLOGIN_METHODS.- A verified user already exists for
alice@example.com. To create one from scratch, usePOST /registration/registerfollowed byPOST /otp/verify-email-otp(same token shape as steps 3-4 below).
Set a base URL for the snippets:
BASE=http://localhost:5312POST /login with an identifier (email or phone). It returns a short-lived ephemeral token
plus the login methods allowed for this user and device.
curl -sS -X POST "$BASE/login" \
-H 'Content-Type: application/json' \
-d '{"identifier":"alice@example.com"}'{
"message": "Login continued",
"token": "<EPHEMERAL_TOKEN>",
"sub": "user-uuid",
"identifierType": "email",
"loginMethods": ["email_otp", "passkey"],
"ttl": 300
}Capture the ephemeral token:
EPHEMERAL=<EPHEMERAL_TOKEN>GET /otp/generate-login-email-otp with the ephemeral token as a Bearer credential sends the code
to the user's email.
To read the code back in a headless flow, add x-seamless-auth-delivery-mode: external. In
development this returns the code directly in a delivery payload. In production, external
delivery also requires a valid x-seamless-service-token from a trusted server adapter; without
it the code is only sent through the configured messaging provider.
curl -sS "$BASE/otp/generate-login-email-otp" \
-H "Authorization: Bearer $EPHEMERAL" \
-H 'x-seamless-auth-delivery-mode: external'{
"message": "OTP sent",
"delivery": { "kind": "otp_email", "to": "alice@example.com", "token": "ABCDEF" }
}Without external delivery mode, the response is just { "message": "OTP sent" } and you read the
code from the email itself.
POST /otp/verify-login-email-otp with the ephemeral token as the Bearer credential and the code
in the body. On success it returns the access token and refresh token.
curl -sS -X POST "$BASE/otp/verify-login-email-otp" \
-H "Authorization: Bearer $EPHEMERAL" \
-H 'Content-Type: application/json' \
-d '{"verificationToken":"ABCDEF"}'{
"message": "Login successful",
"token": "<ACCESS_TOKEN>",
"refreshToken": "<REFRESH_TOKEN>",
"sub": "user-uuid",
"roles": ["user"],
"email": "alice@example.com",
"ttl": 1800,
"refreshTtl": 3600
}ACCESS=<ACCESS_TOKEN>
REFRESH=<REFRESH_TOKEN>Present the access token as a Bearer credential.
curl -sS "$BASE/users/me" -H "Authorization: Bearer $ACCESS"{
"user": {
"id": "user-uuid",
"email": "alice@example.com",
"phone": null,
"roles": ["user"]
},
"credentials": []
}POST /refresh takes the refresh token in the Authorization header (not the body) and
returns a new access token and a new refresh token.
curl -sS -X POST "$BASE/refresh" -H "Authorization: Bearer $REFRESH"{
"message": "Token refreshed",
"token": "<NEW_ACCESS_TOKEN>",
"refreshToken": "<NEW_REFRESH_TOKEN>",
"sub": "user-uuid",
"ttl": 1800,
"refreshTtl": 3600
}Refresh tokens are rotated: the token you just sent is invalidated and replaced by the new
one. Reusing an old refresh token is treated as a compromise signal and revokes the session chain,
so always store the newest refreshToken from each refresh.
- The access token is a signed JWT; verify it against the JWKS at
GET /.well-known/jwks.json(RS256). - This API returns tokens only in JSON bodies and never sets cookies. In a browser context, keep the refresh token out of JavaScript-accessible storage by terminating the session in a trusted server adapter or backend instead of integrating directly. See Deployment Topology.