message.comDevelopers

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

POST/api/v1/auth/registerAuth: Bearer

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

FieldTypeDescription
emailrequiredstringAgent email. Becomes the admin contact for the workspace.
passwordrequiredstringMinimum 10 characters; we score with zxcvbn and reject scores below 3.
workspaceNamerequiredstringDisplay name for the workspace. Shown in the dashboard top bar.
siteUrloptionalstringThe primary site you plan to install on. Used to pre-seed the first site record.
Body
{
  "email": "[email protected]",
  "password": "a-strong-password",
  "workspaceName": "Acme Support",
  "siteUrl": "https://acme.com"
}
201 Created
{
  "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

POST/api/v1/auth/loginAuth: Bearer

Exchanges email + password for a workspace JWT.

Body
{
  "email": "[email protected]",
  "password": "your-password"
}

Code samples

cURL
curl -X POST 'https://app.message.com/api/v1/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"your-password"}'
JavaScript
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();
Python
import requests
r = requests.post(
    "https://app.message.com/api/v1/auth/login",
    json={"email": email, "password": password},
)
token = r.json()["token"]
Ruby
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
<?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

POST/api/v1/auth/forgot-passwordAuth: Bearer

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.

Body
{ "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

POST/api/v1/auth/reset-passwordAuth: Bearer

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.

Body
{
  "token": "the-token-from-the-email",
  "newPassword": "a-new-strong-password"
}

Logout

POST/api/v1/auth/logoutAuth: Bearer

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

CodeWhen
400 invalid_bodyPayload failed validation (password too weak, email malformed).
401 invalid_credentialsEmail + password mismatch. Generic on purpose.
409 email_takenRegister: that email is already an agent on another workspace.
410 reset_token_expiredReset: the token has expired or was superseded.
429 rate_limitedPer-IP or per-email cap hit. See rate-limit table.

Per-route caps

EndpointCap
POST /auth/register5/min per IP
POST /auth/login5/min per email
POST /auth/forgot-password3/min per IP
POST /auth/reset-password5/min per IP

Common pitfalls

  • Treating 401 from /login as "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.