# Authentication

Source: https://jointextkit.com/docs/authentication
Index: https://jointextkit.com/llms.txt

Every request is authenticated with an API key in the `Authorization` header:

```bash
Authorization: Bearer ssms_sk_test_...
```

## Test and live keys

| Prefix | Mode | Behavior |
| --- | --- | --- |
| `ssms_sk_test_` | Sandbox | Instant and free. Messages are simulated (`"simulated": true`): no text is sent, except to your own verified phone. See [Sandbox](/docs/sandbox#simulated-or-real). |
| `ssms_sk_live_` | Live | Real numbers and real delivery. Mintable once your account has live access. |

Keys issued with an older prefix (`dsms_sk_*`, `resms_sk_*`)
are still accepted and will never be revoked for their prefix; new keys mint
as `ssms_sk_`.

Keys never expire, but you can roll or revoke them anytime from the
[console](/console/keys). Rolling revokes the old key immediately
and mints a replacement.

## Storage

We look keys up by a keyed hash (HMAC-SHA256 under a secret held on our
servers). Each active key is also kept encrypted (AES-256-GCM, under a
separate key held on our servers) so an admin can view and copy it again from
the console. Every view is in the [audit log](/console/audit-log). Revoking or
rolling a key deletes its encrypted copy. Keys made before October 27, 2026
can't be viewed again: roll one to get a key you can.

## Scopes

A key has full access unless you restrict it. In
[Console → API keys](/console/keys), choose **Restricted** and tick the scopes
the key needs; a key that leaks can then only do what its job required.
Rolling a key keeps its scopes.

| Scope | Allows |
| --- | --- |
| `messages:send` | `POST /v1/messages`, `POST /v1/test/inbound` |
| `messages:read` | `GET /v1/messages`, `GET /v1/messages/{id}`, `GET /v1/deliverability` |
| `numbers:read` | `GET /v1/numbers`, `GET /v1/numbers/available` |
| `numbers:write` | `POST /v1/numbers`, `PATCH /v1/numbers/{id}`, `DELETE /v1/numbers/{id}` |
| `verify` | `/v1/verify`, `/v1/verify/check`, `/v1/verify/{id}` |
| `consent` | `/v1/consent` and everything under it |
| `lookup` | `GET /v1/lookup/{phone}` |
| `webhooks` | `GET /v1/events`, `GET /v1/events/wait`, `POST /v1/events/{id}/replay`, `GET /v1/webhooks`, `GET /v1/webhooks/deliveries` |
| `webhooks:write` | `POST /v1/webhooks`, `GET`/`PATCH`/`DELETE /v1/webhooks/{id}`, `POST /v1/webhooks/{id}/rotate_secret`, `/test` and `/replay` (creating an endpoint or rotating its secret returns the secret) |
| `customers` | `/v1/customers` and everything under it |
| `contacts` | `/v1/contacts`, `/v1/lists`, `/v1/segments`, `/v1/topics` and everything under them |
| `registrations` | `/v1/registrations` and everything under it |
| `billing` | `GET /v1/balance`, `GET /v1/usage`, `GET`/`PATCH /v1/spend-limit` |
| `automations` | `POST /v1/track`, `/v1/automations` and everything under it |
| `audit_logs:read` | `GET /v1/audit-logs`, `GET /v1/exports/audit_log` |
| `links` | `/v1/link-domains` and everything under it ([link shortening](/docs/messages#link-shortening); sending with `shorten_links` needs `messages:send`) |
| `keys` | `GET`/`POST /v1/api_keys`, `DELETE /v1/api_keys/{id}` |

A restricted key calling outside its scopes gets:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key is missing the `messages:send` scope. Use a key with that scope, or mint one in the console (API keys).",
    "required_scope": "messages:send"
  }
}
```

Keys minted before scopes existed, and keys minted without choosing any, keep
full access.

## Keys for the apps you build

A key can create keys, so an agent building an app (in Replit, Claude Code or
Cursor) can give the app its own key to keep as a secret:

```bash
curl https://api.jointextkit.com/v1/api_keys \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Booking app", "scopes": ["messages:send", "verify"]}'
```

The response has the `secret`, once. A key only creates keys of its own mode
(a test key makes test keys) and never a broader one: a key with limited
scopes can only create keys with scopes inside its own. Every new key emails
the account's owner and admins and is in the audit log. `GET /v1/api_keys`
lists the keys of your mode; `DELETE /v1/api_keys/{id}` revokes one. 20 new
keys an hour.

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `invalid_api_key` | Missing, malformed, revoked, or unknown key. |
| 403 | `key_paused` | We paused this live key because it suddenly made far more requests than usual from a country it had not been used from. An admin can resume it, or roll it, under **API keys**. See [unusual key activity](/docs/security#unusual-api-key-activity). |
| 403 | `live_access_required` | Live key used before live access was granted, a live-only endpoint hit with a test key, or a test key reaching data the whole account shares (contacts, consent, templates, customers, registrations, pools, the spend limit, the audit log) on an account with live access. |
| 403 | `insufficient_scope` | A restricted key called an endpoint outside its scopes; `required_scope` names the one it needs. |
| 403 | `tenant_suspended` | The account is suspended. |
| 403 | `account_closed` | The account is closed. Every key of a closed account is revoked too. |
| 403 | `email_unconfirmed` | The account's email isn't confirmed yet, so it can't create keys or webhooks or connect integrations. Confirm it in the console with the emailed code. |
