Authentication
Every request carries a bearer credential. There are two kinds, and they are not interchangeable.
Authorization: Bearer tm_live_a1b2c3…API keys
For your servers. A key belongs to exactly one account and carries a set of scopes. The account is never a field in the request — it is carried by the credential, which is what stops one customer reading another's data by changing an id.
| Prefix | Mode |
|---|---|
tm_live_ | Live. Sends real messages and costs real money. |
tm_test_ | Test. Sends nothing, costs nothing, behaves identically. |
Test mode is a property of the key, not of the URL or a flag in the body. That matters: a flag can be forgotten in one code path, and the consequence of forgetting it is a real text to a real person.
Keys cannot cross the boundary. A live key cannot read test messages and a test key cannot read live ones — a request for a message in the other world gets a 404, not a403, because telling a test key that a live message exists is itself the disclosure. That is the half of test mode people forget: test keys end up in CI, on laptops and in .env files, so one leaking must not expose real recipients.
Every message carries mode, live or test, so a sandbox message can never be mistaken for a real one in a log. In the dashboard you see live by default and can switch — you are a person, not a key, and it is your own account.
Scopes
| Scope | Allows |
|---|---|
messages:send | Sending |
messages:read | Reading messages and their status |
contacts:read | Reading contacts |
contacts:write | Creating and importing contacts |
suppressions:read | Reading your opt-out list |
suppressions:write | Adding and removing opt-outs |
Give a key only what its job needs. A booking system that sends confirmations has no reason to be able to read your contact list, and the day that key leaks is the day you find out whether you agreed.
The secret is shown once
We store a hash. There is no "show key" button because there is nothing to show — if we could display it back to you, so could anyone who got into our database. Lose it and you revoke and mint another; it takes ten seconds.
The same endpoints, either way
Endpoints that are both a public API and a dashboard screen — the message log, the opt-out list — accept either credential at the same URL. You are never asked to learn a second path for the same resource. Scopes apply to keys only, because only keys have them; a signed-in person is bounded by their role instead.
Session tokens
For the dashboard and the mobile app, not for your servers. Short-lived access token, rotating refresh token.
Refresh tokens rotate and are single-use. Each one can be exchanged exactly once. Presenting a token that has already been used revokes the entire family, on the assumption that it was stolen — the legitimate holder and the thief both get signed out, which is the right outcome when we cannot tell which is which.
A refresh token cannot authenticate an ordinary request, and an access token cannot mint a new refresh token. Those are separate abilities on purpose: without that split, one stolen access token could be turned into indefinite access.
What a failed credential looks like
HTTP/1.1 401 Unauthorized
{
"error": {
"type": "authentication",
"code": "unauthenticated",
"message": "This endpoint requires authentication.",
"doc_url": "https://docs.textmast.com/errors/unauthenticated",
"request_id": "req_01j8xk2p7n4qc3m9v0wz5bt6ay"
}
}A key that worked yesterday and 401s today has usually been revoked rather than expired — API keys do not expire on their own. Check the API keys screen before debugging your client.
Storing keys
- Server side only. A key in a mobile app or a browser bundle is a published key.
- Environment variables or a secret manager, never source control.
- One key per integration, so you can revoke one without taking down the others.
- If you think a key leaked, revoke it. It is free, immediate and reversible by minting a new one.