Skip to main content

Participant authentication

No password. The same pair of endpoints serves someone who has never used the app and someone who already has an account — the API decides internally whether to create or just authenticate.

EndpointWhat it does
POST /v1/auth/wallet/otp/requestSends the code by SMS. Requires a Cloudflare Turnstile challengeToken. The response is always neutral — it doesn't reveal whether the phone already had an account.
POST /v1/auth/wallet/otp/verifyConfirms the code. Creates the identity if the phone is new, or authenticates the existing one. Returns accessToken/refreshToken/profile in the body and sets the wallet_user_* cookies.
POST /v1/auth/wallet/refreshRotates the refresh token (body, X-Refresh-Token header, or cookie). Rejects a refresh token from another profile.
POST /v1/auth/wallet/logoutRevokes the current session and clears the three wallet_user_* cookies.

Examples

The examples use http://127.0.0.1:8787 (the local wallet-edge). In production, swap in your environment's public Edge URL.

curl -X POST http://127.0.0.1:8787/v1/auth/wallet/otp/request \
-H "Content-Type: application/json" \
-d '{
"phone": "+5511999999999",
"challengeToken": "<Turnstile token>"
}'

Once the SMS arrives, confirm the code:

curl -X POST http://127.0.0.1:8787/v1/auth/wallet/otp/verify \
-H "Content-Type: application/json" \
-d '{
"phone": "+5511999999999",
"token": "123456"
}'

Phone must be in E.164

phone is always in the format +5511999999999 (with +, country code, no spaces or dashes). Supabase — which issues the OTP under the hood — stores the phone without the + internally; this is already handled in the backend, the client just needs to always send it with +.

profileComplete: false — what to do

When verify responds with profile.profileComplete === false, the phone is new and the profile doesn't have a name yet. In that case, collect the name and call:

PATCH /v1/auth/wallet/me
Authorization: Bearer <accessToken>
Content-Type: application/json

{ "displayName": "Maria Silva" }

See Profile for the full contract of that endpoint.

Rate limiting

otp/request is limited both by IP and by a hash of the normalized phone number — a single number can't get an endless stream of SMS even by switching IPs, and one IP can't flood different numbers without hitting the per-IP limit. otp/verify also has its own limit, to make brute-forcing the 6-digit code harder.