Send Ory SMS codes through TextKit
Ory's identity service sends its texts through a courier channel that you can point at any HTTP API. Give it an sms channel that calls TextKit, and every code Ory sends by text, for sign-in, account verification or account recovery, goes out through TextKit. Ory still creates and checks the codes. The configuration is the same on Ory Network and on self-hosted Ory Kratos.
Before you start
- An Ory project. To sign in by text, turn on one-time codes under Authentication > Passwordless login (
selfservice.methods.code.passwordless_enabled: true), and mark the phone trait in your identity schema as a code identifier sentvia: "sms". - The Ory CLI, signed in, if you configure from the terminal.
- A TextKit test key (
ssms_sk_test_...). Sign up and copy one from the console. Make it a restricted key with only themessages:sendscope (scopes): the key sits in another service's settings, and a key that leaks can then only send texts.
1. The request body
Ory builds each request from a Jsonnet template. This one maps Ory's fields onto TextKit's:
function(ctx) {
to: ctx.recipient,
body: ctx.body,
category: "otp",
}ctx.recipient is the phone number and ctx.body the rendered text with the code in it. Ory has text templates for three messages only, login_code, verification_code and recovery_code, and all three carry a code, so the template labels every send otp.
TextKit sends one-time codes at any hour, and it decides what is a code from the text's wording, not from category. Digits within a few words of a word that names them ("Your Acme code is 482913") count. A text that does not read as a code is held overnight like any other message (quiet hours). If you edit the template, keep the word "code" next to the digits.
2. The channel
In the Ory Console the channel lives under Authentication > SMS Configuration. In configuration it is a courier channel with the template base64-encoded:
courier:
channels:
- id: sms
type: http
request_config:
url: https://api.jointextkit.com/v1/messages
method: POST
body: base64://ZnVuY3Rpb24oY3R4KSB7CiAgdG86IGN0eC5yZWNpcGllbnQsCiAgYm9keTogY3R4LmJvZHksCiAgY2F0ZWdvcnk6ICJvdHAiLAp9Cg==
headers:
Content-Type: application/json
auth:
type: api_key
config:
name: Authorization
value: Bearer ssms_sk_test_...
in: headerThe api_key auth type sets a header named name to value, here Authorization: Bearer .... The key is stored in your Ory configuration, which is one more reason to give it only the messages:send scope. On Ory Network, export the configuration, add the channel, and apply it:
ory get identity-config --project <project-id> --workspace <workspace-id> --format yaml > updated_config.yaml
# add the courier channel above, then:
ory update identity-config --project <project-id> --workspace <workspace-id> --file updated_config.yaml3. Test in the sandbox
With the test key in place, start a sign-in or verification flow for an identity whose phone trait holds the test number, and check the code in TextKit's Logs.
- Your own phone. Verify your cell in the console's settings and use it as the number. The text arrives for real and ends with "- via TextKit sandbox". Real sandbox texts are capped at 10 a day.
+15005550006. Always endsdelivered, and nothing reaches a phone. Any other real-looking number is simulated too: the message shows in the console's Logs asTest · simulated.+15005550015. A recipient for whom it is always night. Only a text that reads as a one-time code is delivered, so this is how you check your template's wording.+15005550011. Answers403 recipient_opted_out, which shows you what your users see when a send is refused.
See sandbox and test numbers for every magic number.
The courier treats any 2xx answer as sent. For any other answer Ory logs the status and the start of the response body, which holds TextKit's error code, such as recipient_opted_out or invalid_request.
Go live
- Swap in a live key (
ssms_sk_live_...) in the channel'sauth.config.value, then apply the configuration again. Keep it restricted tomessages:send. - Have a number that can text anyone: a local number on an approved 10DLC registration, or a verified toll-free number. We file the registration for you: see compliance and registration. A registration for two-factor codes fits a sign-in flow best. With one live number, leave
fromout and TextKit sends from it. Once you have a second, addfrom. - Add credits. Free accounts text only numbers they have verified, and your first credit purchase lifts that (credits).
- Send your code template early. In your first 30 days of live sending, our team reviews each new kind of message before it goes out. Codes from a two-factor registration are exempt. Otherwise, send one code to a real person before launch so the template is approved by the time your users sign in (review of new kinds of message).
TextKit sends to US and Canadian numbers only. A number from anywhere else answers 400 invalid_request, so limit phone sign-up to those two countries.
Sources
Provider steps were checked against these pages on 2026-10-28: