---
title: SMS delivery
description: Customize SMS delivery process.
sidebar:
  order: 5
---

## 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.

:::info[Important]
- Do not depend on the built-in service for production delivery. Its quota and availability are service policy, not an SDK contract.
- When the service returns HTTP 429, released SDK fallback implementations treat that response as terminal and print the message input. This can include the phone number, OTP, magic link, and code lifetime.
- You cannot customize the SMS content when using this method. If you want to customize the content, please see one of the other methods in this section.
:::

:::caution[Sensitive fallback logs]
OTP codes and magic links are authentication secrets. Prevent production fallback logs from reaching shared consoles or
third-party log pipelines. If you must retain them for testing, restrict access, redact the phone number and secret values,
set a short retention period, and verify deletion. Prefer configuring Twilio or a custom service before production so a
quota response cannot expose message content through this fallback.
:::

### Twilio

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

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
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(),
  ],
});
```
</Tab>
<Tab title="Go" value="go">
```go
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,
				},
			}),
		},
	})
}

```
</Tab>
<Tab title="Python" value="python">
```python check=false reason="Partial configuration example"
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_="...",
                    )
                )
            )
        )
    ]
)
```
</Tab>
</CodeGroup>

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:

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
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(),
  ],
});
```
</Tab>
<Tab title="Go" value="go">
```go
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>"),
				},
			}),
		},
	})
}
```
</Tab>
<Tab title="Python" value="python">
```python check=false reason="Partial configuration example"
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>"))
        )
    ]
)
```
</Tab>
</CodeGroup>

### 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.

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
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(),
  ],
});
```
</Tab>
<Tab title="Go" value="go">
```go
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
					},
				},
			}),
		},
	})
}
```
</Tab>
<Tab title="Python" value="python">
```python check=false reason="Partial configuration example"
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)
        )
    ]
)
```
</Tab>
</CodeGroup>

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.

:::info[Important]
When using this callback, you must manage sending the SMS yourself.
:::

:::note[Error Management]
Throw or return an error from `sendSms` when delivery fails. API-triggered delivery can propagate it through the SDK's
error handler; non-API calls may log it. Do not include phone numbers, OTPs, magic links, provider credentials, or complete
provider responses in exceptions or logs.
:::

## SMS Customization

You can see the default SMS content:
- Default [passwordless login with OTP template](https://github.com/supertokens/email-sms-templates#otp-login-1).
- Default [passwordless login with magic link template](https://github.com/supertokens/email-sms-templates#magic-link-login-1).
- Default [passwordless login with magic link and OTP template](https://github.com/supertokens/email-sms-templates#magic-link--otp-login-1).

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.


<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx check=false reason="Requires surrounding application context"
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(),
  ],
});
```
</Tab>
<Tab title="Go" value="go">
```go
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,
				},
			}),
		},
	})
}
```
</Tab>
<Tab title="Python" value="python">
```python check=false reason="Partial configuration example"
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
                )
            )
        )
    ]
)
```
</Tab>
</CodeGroup>


## 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


<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
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(),
  ],
});
```
</Tab>
<Tab title="Go" value="go">
```go
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
					},
				},
			}),
		},
	})
}
```
</Tab>
<Tab title="Python" value="python">
```python check=false reason="Partial configuration example"
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)
        )
    ]
)
```
</Tab>
</CodeGroup>
