How to send an SMS magic link for one-click phone verification

January 28, 2025
Written by
Reviewed by

How to send an SMS magic link for one-click phone verification

Introduction

SMS verification is incredibly popular for a reason: it's widely accessible, user-friendly and intuitive. However, traditional SMS one-time passcodes (OTPs) require the additional step of typing in the code to a web form. Magic links, or clickable verification links, improve the user experience by creating a one-click verification experience that can be used for phone number verification, login, or authorization.

This tutorial will show you how to implement SMS magic links with the Twilio Verify API. The Verify API is a simple multichannel API for user verification and authentication that will abstract the more annoying parts of sending an SMS (or Voice call, WhatsApp message, email, and more) and let you focus on the business logic.

This information in this tutorial can also be used to send WhatsApp magic links. Register your WhatsApp sender ID to start sending WhatsApp verification messages.

Step 1: The user inputs their phone number on your application OR user logs in and already has a phone number associated with their account.

Step 2: Your application calls the Twilio Verify API /verifications endpoint to send an SMS to the end user. The Verify API populates a template containing your verification URL with a user identifier and the OTP e.g. yourco.biz/verify?id=+14151234567&code=123456.

Step 3: The user receives the SMS and clicks on the link.

Step 4: Your application calls the Twilio Verify API /verification-check endpoint to validate the provided OTP.

Step 5: Your application checks that the verification is approved and sets a session or sends a callback to the initiating page indicating verification success.

To code along with this tutorial you will need:

  1. A Twilio Account: sign up or sign in.
  2. A Twilio Verify Service. Create a Verify Service in the console and make note of the Service SID.
  3. Basic understanding of JavaScript

In your favorite text editor, create a new file called verify.html and add the following code. Save the file somewhere you can easily find it to upload in the next step.

<!DOCTYPE html>
<html lang="en">
 <head>
   <title>One-Click Login with Twilio Verify</title>
 </head>
 <body>
   <div>
     <div id="result"></div>
     <p>
       <a href="/index.html">Try another verification</a>
     </p>
   </div>
 </body>
 <script>
   async function checkVerification() {
     const queryParams = new URLSearchParams(window.location.search);
     const data = new URLSearchParams();
     ["code", "id"].forEach((key) => {
       if (queryParams.has(key)) {
         data.append(key, queryParams.get(key));
       }
     });
     let response = await fetch("./verify/check", {
       method: "POST",
       body: data,
     });
     let json = await response.json();
     document.getElementById("result").innerHTML = json.message;
   }
   checkVerification();
 </script>
</html>

Head over to the Twilio Code Exchange and deploy this sample project as a starting point. This project will provide a few things:

  1. A phone number input field and general boilerplate
  2. A live URL you can use for your SMS magic link

After you deploy your project, click the option to Edit your customizations directly in code. This will take you to the functions editor in the Twilio Console.

Instructions for deploying and editing an application, highlighting the code customization option

Click the Add + button to upload the verify.html file you created. Select "Public" visibility and upload your file. Learn more about assets.

Click Deploy All then copy the URL of your new file. This is what you will use in the next step to create your SMS magic link template.

Dropdown menu in the Assets section showing options to copy URL or delete the verify.html file

Note: as an alternative to Twilio Functions, you can host this code on any publicly accessible hosting provider (so that it can be accessed after clicking a link in an SMS), e.g. Vercel or GitHub pages. Ensure that the URL will not change since it will be hardcoded into a template in the next step.

Request a custom template for your verification message

A custom template is required for using SMS magic links. Templates may take 2-4 weeks to be approved and we appreciate your patience as we validate your template.

Reviewing custom templates allows us to stay compliant with carriers while providing you the benefits of the Verify API like increased throughput, built-in global sender IDs, fraud prevention, and more.

Verify has a variety of predefined templates, but in order to ensure compliance, Twilio requires approval of a custom template with the verification URL users will click on.

To request a custom template, head over to our support site and tell the bot:

> I need to submit a ticket to request a custom Twilio Verify template

Follow the prompts to submit a ticket. In the description of the support ticket make sure you include the following information:

Here's an example ticket description:

Hello! I'm following the instructions in this blog post: twilio.com/blog/sms-magic-link-verification

I would like to register a custom Verify template to send an SMS Magic Link with Verify. 

- Account SID: ACxxxxx
- Verify Service SID: VAxxxxx
- Template: `Your login link: https://<<YOUR DOMAIN HERE>>/verify.html?id={{verification_id}}&code={{code}}`
- Name of the template: Magic Link Template - Dev
- Default locale: English

You can submit requests for multiple custom templates, we encourage at least two templates so you can have:

  1. One template for testing.
  2. One template for production with your branded URL.

Once your template is approved, you can start sending magic links. You can complete the next few steps in preparation for your approved template, but won't be able to test end to end yet.

Head to the Twilio Console functions editor, select your Function Service, and add two environment variables (found under "Settings & More").

  1. VERIFY_SERVICE_SID. Starts with VA, find or create here
  2. VERIFY_TEMPLATE_SID. Starts with HJ, will be in your completed Support ticket, or find here)

Add a new function to send verifications. Click Add + and select Add Function. Name your function verify/start, set the visibility to Public, and add the following code:

exports.handler = async function (context, event, callback) {
 const response = new Twilio.Response();
 response.appendHeader("Content-Type", "application/json");
 try {
   if (typeof event.to === "undefined") {
     throw new Error("Missing parameter; please provide an email.");
   }
   const client = context.getTwilioClient();
   const { to } = event;
   const { VERIFY_SERVICE_SID, VERIFY_TEMPLATE_SID } = context;
   const verification = await client.verify.v2
     .services(VERIFY_SERVICE_SID)
     .verifications.create({
       to: to,
       channel: "sms",
       templateSid: VERIFY_TEMPLATE_SID,
       templateCustomSubstitutions: `{ "verification_id": "${to}" }`,
     });
   response.setStatusCode(200);
   response.setBody({
     success: true,
     message: `Sent verification ${verification.sid} to ${to}.`,
   });
   return callback(null, response);
 } catch (error) {
   console.error(error);
   response.setStatusCode(error.status || 400);
   response.setBody({
     success: false,
     message: error.message,
   });
   return callback(null, response);
 }
};

This code uses the /verifications endpoint to send an SMS with a few additional parameters:

  • templateSid: your custom template SID, this can also be set as a default at the Verify Service level.
  • templateCustomSubstitutions: note that the code will be automatically populated by Twilio, so only provide other variables you want to include in the SMS. You will need some kind of identifier for the user, this example uses the phone number.

In assets/index.html, update the script to call the /verify/start endpoint instead of the existing phone number lookup endpoint. Replace everything from the start of function process… until the closing </script> tag with the following code:

async function sendSms() {
 info.style.display = "none";
 error.style.display = "none";
 const to = phoneInput.getNumber();
 const data = new URLSearchParams();
 data.append("to", to);
 try {
   const response = await fetch("./verify/start", {
     method: "POST",
     body: data,
   });
   const json = await response.json();
   if (json.success) {
     console.log("Successfully sent SMS.");
     info.style.display = "";
     info.innerHTML = `SMS sent to ${to}. Check your messages to complete verification.`;
   } else {
     throw new Error(json.error);
   }
 } catch (err) {
   console.error(err);
   error.style.display = "";
   error.innerHTML = "Error starting verification.";
 }
}
document
 .getElementById("lookup")
 .addEventListener("submit", function (event) {
   event.preventDefault();
   sendSms();
 });

Save and Deploy All and test sending a magic link. Click the three dots next to index.html and select Copy URL to navigate to your application. Enter your phone number to test the application, you should receive an SMS with a link!

Verification Logic

Clicking the link won't validate anything yet, so add the verification logic next.

Add a new function to check verifications. Click Add + and select Add Function. Name your function verify/check, set the visibility to Public, and add the following code:

exports.handler = async function (context, event, callback) {
 const response = new Twilio.Response();
 response.appendHeader('Content-Type', 'application/json');
 try {
   const client = context.getTwilioClient();
   const { id, code } = event;
   const verificationCheck = await client.verify.v2
     .services(context.VERIFY_SERVICE_SID)
     .verificationChecks.create({ to: `+${id}`, code: code });
   if (verificationCheck.status !== 'approved') {
     throw new Error('Incorrect token.');
   }
   response.setStatusCode(200);
   response.setBody({
     success: true,
     message: 'Verification success.',
   });
   return callback(null, response);
 } catch (error) {
   console.log(error);
   response.setStatusCode(error.status || 400);
   response.setBody({
     success: false,
     message: error.message,
   });
   return callback(null, response);
 }
};

Test the flow again, but this time when you click on the SMS link, you will be redirected to an approval screen:

Webpage showing verification success with a link to try another verification

Built-in fraud prevention with Verify

This code uses the Verify API's stateless verification tracking to validate the OTP that was included in the request's query parameter. The Verify API includes several features for both better developer experience and stronger security and fraud prevention, including:

  • OTP management, with single-use OTPs that will be invalid after an approved check.
  • Automatic OTP timeout after 10 minutes (timeout is configurable).

At this point you would mark the user as verified for whatever purpose you're checking. How you manage that in your application will vary, for example:

  • For phone number verification, mark the user's number as verified in your database
  • For login, set a session cookie or use a callback to indicate verification success.

Note that the user may start and check the verification on two different devices. Keep that in mind as you determine how to move forward.

Magic links provide a user-friendly way to quickly validate phone numbers for authentication or authorization. For more verification resources, best practices, and examples, check out the following:

I can't wait to see what you build and secure!