Skip to main content
One-time-passcode (OTP) sign-in is the most common non-wallet login: you send the user a short code by email or SMS, they type it back, and they’re signed in. This page shows how to build that UI end to end for both channels.

Prerequisites

Before this: create and initialize a Dynamic client (see Creating a Dynamic Client, Initializing the Dynamic Client). Enable Email and/or SMS as sign-in methods in your Dynamic dashboard.

How OTP sign-in works

The flow is always two steps, which map directly onto two UI states:
  1. Request a code — collect the user’s email or phone number and call sendEmailOTP / sendSmsOTP. This returns an OTPVerification object (it holds a verificationUUID that ties the code to this attempt). Show the code-entry screen.
  2. Verify the code — collect the code the user received and call verifyOTP with the OTPVerification from step 1. On success the user is authenticated and the SDK stores the session.
verifyOTP handles both sign-in and adding a verified email/phone to an already–signed-in user. It picks the right path from the current session — you call the same function either way.

Email OTP

Drive two screens from the OTPVerification returned by sendEmailOTP: an email form, then a code-entry form. Here they’re wired to plain DOM elements.

SMS OTP

SMS is the same two-step flow, plus a country code. sendSmsOTP takes an ISO country code ('US', 'GB', 'BR', …) and the phone number without the country dial-in prefix. The SDK exports supportedCountries so you can build a picker instead of hard-coding a list.
Build the country picker from supportedCountries rather than hard-coding a list, then send and verify exactly as with email.
Pass the phone number without the country dial-in prefix — the country code you select already supplies it. Sending +1 (or 1) as part of phoneNumber for a US number will produce an unreachable number.

Resend and expiry

Codes expire after a few minutes, so give users a way to request a new one. Re-run the same send call — it returns a fresh OTPVerification that replaces the previous one. Add a short cooldown so users can’t spam the button (and to stay within the rate limits below).

Handling errors

The send (sendEmailOTP / sendSmsOTP) and verify (verifyOTP) calls reject with an APIError (from @dynamic-labs-sdk/client/core) when the backend rejects the request. Don’t treat every rejection the same — the response you show depends on which case it is: Branch on the error so a wrong code keeps the user on the code screen, while a rate limit tells them to wait:
Email verification is rate limited to 3 attempts per 10 minutes per email address to protect deliverability. Surface a clear “try again shortly” message rather than retrying automatically. If users report missing emails, see the Email OTP delivery troubleshooting guide.

After verification

Once verifyOTP resolves, the user is authenticated. In React, useUser() now returns the user; in TypeScript, read the session with client.user. New users often need onboarding, MFA enrollment, or an embedded wallet before entering your app — handle that next in Post-login user setup.

See also

Last modified on July 9, 2026