Auth API Live
The user-facing auth endpoints: sign up a workspace, log in, request a password-reset email, complete a reset, and log out. All five are rate-limited; see Errors, rate limits, versioning.
For agent invitations from inside a workspace, see the Agents API. For verifying or refreshing an existing token, see the Authentication API.
Register
Creates a new workspace and admin agent in one step. Returns a fresh workspace JWT bound to the new agent. The onboarding wizard at app.message.com/register calls this endpoint.
Request body
| Field | Type | Description |
|---|---|---|
| emailrequired | string | Agent email. Becomes the admin contact for the workspace. |
| passwordrequired | string | Minimum 10 characters; we score with zxcvbn and reject scores below 3. |
| workspaceNamerequired | string | Display name for the workspace. Shown in the dashboard top bar. |
| siteUrloptional | string | The primary site you plan to install on. Used to pre-seed the first site record. |
{
"email": "[email protected]",
"password": "a-strong-password",
"workspaceName": "Acme Support",
"siteUrl": "https://acme.com"
}{
"token": "eyJhbGci...",
"expiresAt": "2026-05-20T15:30:00Z",
"agent": {
"id": "uuid",
"email": "[email protected]",
"role": "admin",
"workspaceId": "uuid"
},
"workspace": {
"id": "uuid",
"name": "Acme Support",
"subdomain": "acme-support"
}
}Login
Exchanges email + password for a workspace JWT.
{
"email": "[email protected]",
"password": "your-password"
}Code samples
curl -X POST 'https://app.message.com/api/v1/auth/login' \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"your-password"}'const res = await fetch('https://app.message.com/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const { token, agent } = await res.json();import requests
r = requests.post(
"https://app.message.com/api/v1/auth/login",
json={"email": email, "password": password},
)
token = r.json()["token"]require "net/http"
require "json"
uri = URI("https://app.message.com/api/v1/auth/login")
res = Net::HTTP.post(uri, { email: email, password: password }.to_json, "Content-Type" => "application/json")
token = JSON.parse(res.body)["token"]<?php
$ctx = stream_context_create([
"http" => [
"method" => "POST",
"header" => "Content-Type: application/json",
"content" => json_encode(["email" => $email, "password" => $password]),
],
]);
$body = file_get_contents("https://app.message.com/api/v1/auth/login", false, $ctx);
$token = json_decode($body, true)["token"];Forgot password
Sends a password-reset email to the address (if it matches a real agent). Returns 204 regardless of whether the email exists, so we do not leak account enumeration.
{ "email": "[email protected]" }The email contains a one-time token that expires in 30 minutes. Each new request invalidates the previous token, so the most recent email is always the valid one.
Reset password
Completes a password reset using the token from the email plus the new password. On success, the response is identical to a fresh login: a workspace JWT plus the agent record.
{
"token": "the-token-from-the-email",
"newPassword": "a-new-strong-password"
}Logout
Revokes the bearer token by bumping the agent's tokenVersion. Returns 204. Logout is global: every device signed in as this agent will be force-logged on next request.
Errors
| Code | When |
|---|---|
400 invalid_body | Payload failed validation (password too weak, email malformed). |
401 invalid_credentials | Email + password mismatch. Generic on purpose. |
409 email_taken | Register: that email is already an agent on another workspace. |
410 reset_token_expired | Reset: the token has expired or was superseded. |
429 rate_limited | Per-IP or per-email cap hit. See rate-limit table. |
Per-route caps
| Endpoint | Cap |
|---|---|
POST /auth/register | 5/min per IP |
POST /auth/login | 5/min per email |
POST /auth/forgot-password | 3/min per IP |
POST /auth/reset-password | 5/min per IP |
Common pitfalls
- Treating 401 from
/loginas "wrong password". It might be "email does not exist". We deliberately return one generic error for both. - Storing the password in plaintext anywhere. We bcrypt server-side. Your client should never write the password to disk or to a cookie.
- Showing different UI for "email taken" on register. Pair this with the 409 response and you teach attackers which emails are valid. Use a neutral message on both branches.