Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

SMS delivery

Customize SMS delivery process.

Overview

SuperTokens sends SMS in different authentication scenarios. SMS delivery is configured by the Passwordless recipe. Phone OTP can be used as an MFA factor, but the MFA recipe does not expose a separate SMS-delivery configuration.

The following page shows you how to configure the SMS delivery method and adjust the content that gets sent to your users.

Delivery methods

Default method

If you provide no configuration for SMS delivery, the Passwordless recipe uses the backend SDK’s built-in service at https://api.supertokens.com/0/services/sms. This applies whether the Core is self-hosted or managed.

Twilio

Using this method, you can provide your own Twilio account details to the backend SDK, and the SMS is sent using those.

import supertokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
import { TwilioService } from "supertokens-node/recipe/passwordless/smsdelivery";

supertokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  recipeList: [
    Passwordless.init({
      flowType: "USER_INPUT_CODE",
      contactMethod: "PHONE",
      smsDelivery: {
        service: new TwilioService({
          twilioSettings: {
            accountSid: "...",
            authToken: "...",
            opts: {
              // optionally extra config to pass to Twilio client
            },

            // Use exactly one sender option. This example uses from.
            from: "...",
          },
        }),
      },
    }),
    Session.init(),
  ],
});
import (
	"github.com/supertokens/supertokens-golang/ingredients/smsdelivery"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {

	smsService, err := passwordless.MakeTwilioService(smsdelivery.TwilioServiceConfig{
		Settings: smsdelivery.TwilioSettings{
			AccountSid: "...",
			AuthToken:  "...",
			// Use exactly one sender option. This example uses From.
			From: "...",
		},
	})
	if err != nil {
		panic(err)
	}

	supertokens.Init(supertokens.TypeInput{
		RecipeList: []supertokens.Recipe{
			passwordless.Init(plessmodels.TypeInput{
				ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true},
				FlowType:           "USER_INPUT_CODE",
				SmsDelivery: &smsdelivery.TypeInput{
					Service: smsService,
				},
			}),
		},
	})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import passwordless
from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig
from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig, TwilioSettings


init(
    app_info=InputAppInfo(
        api_domain="...", app_name="...", website_domain="..."),
    framework='...',
    recipe_list=[
        passwordless.init(
            contact_config=ContactPhoneOnlyConfig(),
            flow_type="USER_INPUT_CODE",
            sms_delivery=SMSDeliveryConfig(
                service=passwordless.TwilioService(
                    twilio_settings=TwilioSettings(
                        account_sid="...",
                        auth_token="...",
                        opts={
                            # Optional configs to pass to twilio client
                        },

                        # Use exactly one sender option. This example uses from_.
                        from_="...",
                    )
                )
            )
        )
    ]
)

To learn about how to customize the SMS templates, please see the next section.

SuperTokens SMS service

The backend SDKs also expose an API-key-based SuperTokensSMSService. It calls an external SMS endpoint directly and can be used whether your Core is self-hosted or managed. Availability, pricing, credits, quotas, sender identity, key issuance, and the Dashboard workflow are mutable service policy. Confirm them in your current Dashboard or contract before adopting this option; they are not guaranteed by the released SDK interface.

If you have been issued an SMS API key, set it in the backend SDK configuration:

import supertokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
import { SupertokensService } from "supertokens-node/recipe/passwordless/smsdelivery";

supertokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  recipeList: [
    Passwordless.init({
      flowType: "USER_INPUT_CODE",
      contactMethod: "PHONE",
      smsDelivery: {
        service: new SupertokensService("<SMS API KEY GOES HERE>"),
      },
    }),
    Session.init(),
  ],
});
import (
	"github.com/supertokens/supertokens-golang/ingredients/smsdelivery"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	supertokens.Init(supertokens.TypeInput{
		RecipeList: []supertokens.Recipe{
			passwordless.Init(plessmodels.TypeInput{
				ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true},
				FlowType:           "USER_INPUT_CODE",
				SmsDelivery: &smsdelivery.TypeInput{
					Service: passwordless.MakeSupertokensSMSService("<SMS API KEY GOES HERE>"),
				},
			}),
		},
	})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import passwordless
from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig
from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig

init(
    app_info=InputAppInfo(
        api_domain="...", app_name="...", website_domain="..."),
    framework='...',
    recipe_list=[
        passwordless.init(
            contact_config=ContactPhoneOnlyConfig(),
            flow_type="USER_INPUT_CODE",
            sms_delivery=SMSDeliveryConfig(
                service=passwordless.SuperTokensSMSService("<SMS API KEY GOES HERE>"))
        )
    ]
)

Custom method

This method allows you to send messages however you like. The input to the send function consists of SMS template variables, allowing you to create the content of the SMS as well. Use this method if you are:

  • Using a third-party SMS service that is not Twilio.
  • You want to use another delivery method like WhatsApp or Facebook Messenger.
  • You want to do some custom spam protection before sending the SMS.
  • You already have an SMS sending infrastructure and want to use that.
import supertokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";

supertokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  recipeList: [
    Passwordless.init({
      flowType: "USER_INPUT_CODE",
      contactMethod: "PHONE",
      smsDelivery: {
        override: (originalImplementation) => {
          return {
            ...originalImplementation,
            sendSms: async function ({
              codeLifetime, // amount of time the code is alive for (in MS)
              phoneNumber,
              urlWithLinkCode, // magic link
              userInputCode, // OTP
            }) {
              // TODO: create and send SMS
            },
          };
        },
      },
    }),
    Session.init(),
  ],
});
import (
	"fmt"

	"github.com/supertokens/supertokens-golang/ingredients/smsdelivery"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	supertokens.Init(supertokens.TypeInput{
		RecipeList: []supertokens.Recipe{
			passwordless.Init(plessmodels.TypeInput{
				ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true},
				FlowType:           "USER_INPUT_CODE",
				SmsDelivery: &smsdelivery.TypeInput{
					Override: func(originalImplementation smsdelivery.SmsDeliveryInterface) smsdelivery.SmsDeliveryInterface {

						(*originalImplementation.SendSms) = func(input smsdelivery.SmsType, userContext supertokens.UserContext) error {
							// amount of time the code is alive for (in MS)
							codeLifetime := input.PasswordlessLogin.CodeLifetime
							phoneNumber := input.PasswordlessLogin.PhoneNumber

							// magic link
							urlWithLinkCode := input.PasswordlessLogin.UrlWithLinkCode

							// OTP
							userInputCode := input.PasswordlessLogin.UserInputCode
							fmt.Println(codeLifetime)
							fmt.Println(phoneNumber)
							fmt.Println(urlWithLinkCode)
							fmt.Println(userInputCode)
							// TODO: create and send SMS
							return nil
						}

						return originalImplementation
					},
				},
			}),
		},
	})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe.passwordless.types import SMSDeliveryOverrideInput, SMSTemplateVars
from supertokens_python.recipe import passwordless
from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig
from typing import Dict, Any
from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig


def custom_sms_deliver(original_implementation: SMSDeliveryOverrideInput) -> SMSDeliveryOverrideInput:
    async def send_sms(template_vars: SMSTemplateVars, user_context: Dict[str, Any]) -> None:
        # amount of time the code is alive for (in MS)
        _ = template_vars.code_life_time
        __ = template_vars.phone_number
        ___ = template_vars.url_with_link_code  # magic link
        ____ = template_vars.user_input_code  # OTP

        # TODO: create and send SMS...
    original_implementation.send_sms = send_sms
    return original_implementation


init(
    app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
    framework='...',
    recipe_list=[
        passwordless.init(
            contact_config=ContactPhoneOnlyConfig(),
            flow_type="USER_INPUT_CODE",
            sms_delivery=SMSDeliveryConfig(override=custom_sms_deliver)
        )
    ]
)

If you call the original implementation function for sendSms, it uses the service that you have configured. If you have not configured any service, it uses the default service.

SMS Customization

You can see the default SMS content:

To change the content of the default SMS templates, you can override the getContent function in the smsDelivery object. It allows you to return an object that has the following properties:

  • body: The SMS message body.
  • toPhoneNumber: The phone number where the SMS is sent to.
import supertokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
import { TwilioService } from "supertokens-node/recipe/passwordless/smsdelivery";

supertokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  recipeList: [
    Passwordless.init({
      flowType: "USER_INPUT_CODE",
      contactMethod: "PHONE",
      smsDelivery: {
        service: new TwilioService({
          twilioSettings: {
            /*...*/
          },
          override: (originalImplementation) => {
            return {
              ...originalImplementation,
              getContent: async function ({
                isFirstFactor,
                codeLifetime, // amount of time the code is alive for (in MS)
                phoneNumber,
                urlWithLinkCode, // magic link
                userInputCode, // OTP
              }) {
                if (isFirstFactor) {
                  // send some custom SMS content
                  return {
                    toPhoneNumber: phoneNumber,
                    body: "SMS BODY",
                  };
                } else {
                  // for second factor, urlWithLinkCode will always be
                  // undefined since we only support OTP based for second factor
                  return {
                    toPhoneNumber: phoneNumber,
                    body: "SMS BODY",
                  };
                }

                // You can even call the original implementation and
                // modify its content:

                /*let originalContent = await originalImplementation.getContent(input)
                                originalContent.body = "My custom body";
                                return originalContent;*/
              },
            };
          },
        }),
      },
    }),
    Session.init(),
  ],
});
import (
	"fmt"

	"github.com/supertokens/supertokens-golang/ingredients/smsdelivery"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	smsService, err := passwordless.MakeTwilioService(smsdelivery.TwilioServiceConfig{
		Settings: smsdelivery.TwilioSettings{ /* ... */ },
		Override: func(originalImplementation smsdelivery.TwilioInterface) smsdelivery.TwilioInterface {
			// originalGetContent := *originalImplementation.GetContent

			(*originalImplementation.GetContent) = func(input smsdelivery.SmsType, userContext supertokens.UserContext) (smsdelivery.SMSContent, error) {
				// amount of time the code is alive for (in MS)
				codeLifetime := input.PasswordlessLogin.CodeLifetime
				phoneNumber := input.PasswordlessLogin.PhoneNumber

				// magic link
				urlWithLinkCode := input.PasswordlessLogin.UrlWithLinkCode

				// OTP
				userInputCode := input.PasswordlessLogin.UserInputCode
				fmt.Println(codeLifetime)
				fmt.Println(phoneNumber)
				fmt.Println(urlWithLinkCode)
				fmt.Println(userInputCode)

				// send custom SMS content
				return smsdelivery.SMSContent{
					Body:          "SMS BODY",
					ToPhoneNumber: phoneNumber,
				}, nil

				// Or call the original implementation and change its content:
				/*
				   originalResponse, err := originalGetContent(input, userContext)
				   if err != nil {
				       return smsdelivery.SMSContent{}, nil
				   }
				   originalResponse.body = "SMS Body"
				   return originalResponse
				*/
			}

			return originalImplementation
		},
	})
	if err != nil {
		panic(err)
	}

	supertokens.Init(supertokens.TypeInput{
		RecipeList: []supertokens.Recipe{
			passwordless.Init(plessmodels.TypeInput{
				ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true},
				FlowType:           "USER_INPUT_CODE",
				SmsDelivery: &smsdelivery.TypeInput{
					Service: smsService,
				},
			}),
		},
	})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import passwordless
from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig
from supertokens_python.recipe.passwordless.types import TwilioOverrideInput, SMSTemplateVars
from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig, SMSContent, TwilioSettings
from typing import Dict, Any


def custom_sms_content_override(original_implementation: TwilioOverrideInput) -> TwilioOverrideInput:

    # original_get_content = original_implementation.get_content

    async def get_content(template_vars: SMSTemplateVars, user_context: Dict[str, Any]) -> SMSContent:
        # amount of time the code is alive for (in MS)
        _ = template_vars.code_life_time
        phone_number = template_vars.phone_number
        __ = template_vars.url_with_link_code  # magic link
        ___ = template_vars.user_input_code  # OTP

        # send custom SMS content
        return SMSContent(body="SMS BODY", to_phone=phone_number)

        # you can even call the original implementation and modify that

        # original_content = await original_get_content(template_vars, user_context)
        # original_content.body = "My custom body"
        # return original_content

    original_implementation.get_content = get_content
    return original_implementation


init(
    app_info=InputAppInfo(
        api_domain="...", app_name="...", website_domain="..."),
    framework='...',
    recipe_list=[
        passwordless.init(
            contact_config=ContactPhoneOnlyConfig(),
            flow_type="USER_INPUT_CODE",
            sms_delivery=SMSDeliveryConfig(
                service=passwordless.TwilioService(

                    twilio_settings=TwilioSettings(...),
                    override=custom_sms_content_override
                )
            )
        )
    ]
)

Overrides

You can use the override functionality to trigger any kind of behavior before and after SMS sending. This can include things like:

  • Logging
  • Spam protection actions
  • Modifying the SMS template variables before sending messages
import supertokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";

supertokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  recipeList: [
    Passwordless.init({
      flowType: "USER_INPUT_CODE",
      contactMethod: "PHONE",
      smsDelivery: {
        override: (originalImplementation) => {
          return {
            ...originalImplementation,
            sendSms: async function (input) {
              // TODO: before sending SMS

              await originalImplementation.sendSms(input);

              // TODO: after sending SMS
            },
          };
        },
      },
    }),
    Session.init(),
  ],
});
import (
	"github.com/supertokens/supertokens-golang/ingredients/smsdelivery"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	supertokens.Init(supertokens.TypeInput{
		RecipeList: []supertokens.Recipe{
			passwordless.Init(plessmodels.TypeInput{
				ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true},
				FlowType:           "USER_INPUT_CODE",
				SmsDelivery: &smsdelivery.TypeInput{
					Override: func(originalImplementation smsdelivery.SmsDeliveryInterface) smsdelivery.SmsDeliveryInterface {

						originalSendSms := *originalImplementation.SendSms

						(*originalImplementation.SendSms) = func(input smsdelivery.SmsType, userContext supertokens.UserContext) error {
							// TODO: before sending SMS

							err := originalSendSms(input, userContext)
							if err != nil {
								return err
							}

							// TODO: after sending SMS
							return nil
						}

						return originalImplementation
					},
				},
			}),
		},
	})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe.passwordless.types import SMSDeliveryOverrideInput, SMSTemplateVars
from supertokens_python.recipe import passwordless
from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig
from typing import Dict, Any
from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig


def custom_sms_deliver(original_implementation: SMSDeliveryOverrideInput) -> SMSDeliveryOverrideInput:
    original_send_sms = original_implementation.send_sms

    async def send_sms(template_vars: SMSTemplateVars, user_context: Dict[str, Any]) -> None:
        # TODO: before sending SMS

        await original_send_sms(template_vars, user_context)

        # TODO: after sending SMS

    original_implementation.send_sms = send_sms
    return original_implementation


init(
    app_info=InputAppInfo(
        api_domain="...", app_name="...", website_domain="..."),
    framework='...',
    recipe_list=[
        passwordless.init(
            contact_config=ContactPhoneOnlyConfig(),
            flow_type="USER_INPUT_CODE",
            sms_delivery=SMSDeliveryConfig(override=custom_sms_deliver)
        )
    ]
)

API reference

API schema and response details