# auth.md

Source: https://jointextkit.com/auth.md
Index: https://jointextkit.com/llms.txt

You are an agent that wants to call the TextKit API on a user's behalf. This
file describes how to obtain credentials and how to handle them safely.

Two hosts are relevant:

- **Resource server**: `https://api.jointextkit.com/v1`, the API you will call.
- **Dashboard**: `https://jointextkit.com/console`, where the user manages their account and keys.

## The short version

If you can run shell commands, this is the whole setup:

```bash
npx jointextkit login
npx jointextkit listen --forward-to http://localhost:3000/webhooks
```

1. `login` prints a URL and a short code and waits. Show both to the user
   and ask them to open the URL, check the code, and press **Approve** (they
   must be signed in to the console as an account admin). You never see or
   handle a key: a new **test** key is written to `~/.textkit.json` and
   the command prints the account's sandbox number to send from. Add
   `--json` for machine-readable lines; the first one carries
   `verification_url` and `user_code`. The request expires in 10 minutes.
2. `listen` replays the account's events to a local server, signed like
   real webhooks, and prints the signing secret to give the handler
   (`whsec_local_...`). No tunnel and no console setup. Run it in the
   background; `npx jointextkit trigger message.received` makes an event.

After `login`, every `npx jointextkit` command and every SDK call that
reads the stored key works. Details: `https://jointextkit.com/docs/cli.md`.

What this flow does not do: it never issues a live key, and it cannot create
an account. The user signs up at `https://jointextkit.com/signup` (free, no card).
There is no OAuth client registration.

## Use the existing tooling first

Before you do anything credential-shaped, check whether the user has already
wired TextKit into your environment.

1. **MCP server**: if you are an MCP client (Claude, Claude Code, Cursor,
   Codex, etc.), TextKit ships a streamable-HTTP MCP server that exposes
   messages, numbers, verification, lookup, and events as tools. The user
   configures it once with their key; you call the tools and never see the key.
   Endpoint: `https://mcp.jointextkit.com` (also `https://jointextkit.com/api/mcp`).
   Discovery: `https://jointextkit.com/.well-known/mcp.json`.
2. **Agent Skills**: guidance documents that teach you the gotchas (consent and
   opt-out, message segments, quiet hours, 10DLC, webhook signature
   verification, sandbox magic numbers). Index:
   `https://jointextkit.com/.well-known/agent-skills/index.json`. Install with
   `curl -sSL https://jointextkit.com/skills/install.sh | sh`. Load the relevant `SKILL.md` before
   calling the API directly.
3. **CLI**: if you are running shell commands, prefer `npx jointextkit` over
   hand-rolled `curl`. It resolves auth, retries, and takes `--json` for
   machine-readable output. Auth via `npx jointextkit login` (above) or
   `TEXTKIT_API_KEY`.

If any of these is already configured, use it and stop. Do not ask the user for
an API key you do not need.

## If you do need a raw key

For a test key, `npx jointextkit login` is the way: the user approves in
their browser and no secret is pasted anywhere. If you cannot run it, or you
need a live key, ask the user to create one at `https://jointextkit.com/console/keys`
(store a pasted key with `npx jointextkit login --key <key>` rather than
writing it into a file yourself). Sandbox keys are free and issued instantly
at signup; live keys are enabled after a short live-access review, so do not
promise the user live sending on a fresh account.

Webhook endpoints do not need the console either: `POST /v1/webhooks`
creates one and returns its signing secret once
(`https://jointextkit.com/docs/webhooks.md`).

Key shapes:

| Prefix | Mode | Behavior |
| --- | --- | --- |
| `ssms_sk_test_` | Sandbox | Instant and free. Messages are simulated: `status` reaches `delivered` but no text is sent, and the message says `"simulated": true`. One exception: a send to the account owner's own verified phone is really delivered (`"simulated": false`). |
| `ssms_sk_live_` | Live | Real numbers, real delivery, real money. Requires live access. |

Keys with an older prefix (`dsms_sk_*` from when the product was called
Delivered, or `resms_sk_*`) are still accepted on `api.jointextkit.com`; do not
treat them as invalid. The old host `api.deliveredsms.com` is retired (410).

Send the key as a bearer token:

```bash
curl https://api.jointextkit.com/v1/messages \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"+15005550006","body":"hello"}'
```

`from` is optional when the account has one number for the key's mode (a
sandbox key always starts with exactly one). **Never tell the user a test-key
message arrived on a phone** because its status is `delivered`: read
`simulated` on the message. If the response has a `notice`, relay it.

## Handling the key safely

- **Read it from the environment.** `TEXTKIT_API_KEY` is the expected variable.
  Never inline a key into source, a config file you commit, a test fixture, or a
  shell command that lands in history.
- **Never print it.** Not in logs, not in a summary to the user, not in an error
  message, not in a commit message. If you must refer to a key, use its prefix
  and last four characters.
- **Never send it anywhere but `api.jointextkit.com`.** No third-party service,
  no pastebin, no issue tracker, no other model provider.
- **Prefer a test key.** If the task is "build the integration" rather than "send
  this specific message to this specific person", a `ssms_sk_test_` key
  exercises everything a live key does, without sending anything. Ask for a live key only when real delivery is the
  actual goal, and tell the user that is what you are asking for.
- **Keys cannot be re-displayed.** They are stored hashed (SHA-256). If one is
  lost or exposed, roll it at `https://jointextkit.com/console/keys`; rolling revokes the
  old key immediately.
- **If you believe a key has leaked, say so and stop.** Tell the user, recommend
  rolling it, and do not keep using it.

## Errors you will actually hit

| Status | Code | What to do |
| --- | --- | --- |
| 401 | `invalid_api_key` | Missing, malformed, revoked, or unknown key. Do not retry; ask the user for a valid key. |
| 403 | `live_access_required` | A live endpoint was hit with a test key, or live access is not granted yet. Do not retry. |
| 403 | `tenant_suspended` | The account is suspended. Stop and tell the user. |
| 403 | `account_closed` | The account is closed. Stop and tell the user; only support can reopen it. |
| 429 | `rate_limited` | Back off and retry with the `Retry-After` header. |

Full error envelope and every code: `https://jointextkit.com/docs/errors.md`.

## Before you send anything real

A live key sends real messages to real people and costs real money. The
recipient rules are not optional:

- Only message recipients who opted in. You are not the one who obtained
  consent, so confirm with the user that they did.
- Honor STOP / opt-out. TextKit blocks sends to a number that opted out of
  this account and returns 403 on `POST /v1/messages`; one-time passcodes are
  exempt so a login code still reaches the user. Subscribe to
  `message.opted_out` and mirror it in your own store. Details:
  `https://jointextkit.com/docs/opt-out.md`.
- Do not send at a volume or cadence the user has not asked for. If a task
  implies a bulk send, confirm the recipient count with the user first.

See `https://jointextkit.com/skills/sms-best-practices/SKILL.md` before your first live send.

## Everything else

- Full site and doc index: `https://jointextkit.com/llms.txt`
- Complete documentation in one file: `https://jointextkit.com/docs/llms-full.txt`
- OpenAPI spec: `https://jointextkit.com/openapi.json` / `https://jointextkit.com/openapi.yaml`
