# Send Ory SMS codes through TextKit

Source: https://jointextkit.com/integrations/ory
Index: https://jointextkit.com/llms.txt

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 sent `via: "sms"`.
- The Ory CLI, signed in, if you configure from the terminal.
- A TextKit test key (`ssms_sk_test_...`). [Sign up](/signup) and copy one from the console. Make it a restricted key with only the `messages:send` scope ([scopes](/docs/authentication)): 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:

```jsonnet
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](/docs/compliance-checks#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:

```yaml
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: header
```

The `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:

```bash
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.yaml
```

## 3. 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 ends `delivered`, and nothing reaches a phone. Any other real-looking number is simulated too: the message shows in the console's Logs as `Test · 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`.** Answers `403 recipient_opted_out`, which shows you what your users see when a send is refused.

See [sandbox and test numbers](/docs/sandbox) 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

1. **Swap in a live key** (`ssms_sk_live_...`) in the channel's `auth.config.value`, then apply the configuration again. Keep it restricted to `messages:send`.
2. **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](/docs/compliance). A registration for two-factor codes fits a sign-in flow best. With one live number, leave `from` out and TextKit sends from it. Once you have a second, add `from`.
3. **Add credits.** Free accounts text only numbers they have verified, and your first credit purchase lifts that ([credits](/docs/credits)).
4. **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](/docs/compliance-checks#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:

- [Ory: Send SMS to your users](https://www.ory.com/docs/kratos/emails-sms/sending-sms)
- [Ory: Passwordless one-time code](https://www.ory.com/docs/kratos/passwordless/one-time-code)
- [Ory Kratos: configuration schema (api_key auth)](https://github.com/ory/kratos/blob/master/embedx/config.schema.json)
- [Ory Kratos: courier HTTP channel](https://github.com/ory/kratos/blob/master/courier/http_channel.go)
