Files
sso-manager-node/API.md
T
wmantly 4e5a2aa4f9 Add plain-language concept docs; fix docs viewer rendering; link API tokens
- New docs/concepts-{accounts,oauth-apps,api-tokens}.md -- plain-language
  guides aimed at less technical readers, each linking onward to the
  existing schema/protocol-level doc for anyone who wants that detail.
  Card help links (Users, Groups, OAuth cards, My groups, Members of
  <uid>'s group) now point here instead of straight at the technical
  docs; the LDAP-protocol-wiring cards (raw connection details for
  connecting a 3rd-party app) stay pointed at the technical ldap.md,
  since that's genuinely the right depth for that task.
- The "New API Token" card had no help link at all -- added, pointing to
  the new API Tokens doc.
- Fixed the in-app docs viewer rendering every docs/*.md page with a
  garbled heading + stray <hr> at the top: Jekyll front matter (meant
  only for the GitHub Pages build) was never stripped before being
  handed to the markdown renderer. Also fixed: cross-doc links
  (ldap.html, index.html, etc.) never resolved in-app, since this
  viewer serves docs at /docs/<slug> with no .html suffix -- rewritten
  to the correct in-app URL, same idea as the existing image-path fix.

Bumps to v1.1.12.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 21:49:27 -04:00

1157 lines
24 KiB
Markdown

# SSO Manager API Documentation
> Looking for a plainer explanation of what API tokens are and when you'd
> want one, instead of a full endpoint reference? See
> [API Tokens](/docs/api-tokens) (in-app) or
> [concepts-api-tokens.md](docs/concepts-api-tokens.md) (repo).
## Overview
API documentation for the SSO Manager Node application. Provides endpoints for authentication, user management, group management, token management, notifications, and OAuth 2.0 / OpenID Connect.
**Base URL:** `https://your-domain.com`
**Content Type:** `application/json` (all request and response bodies)
---
## Authentication
### Getting a Token
Login via `POST /api/auth/login` to receive an auth token.
### Using the Token
Include the token in every protected request:
```
auth-token: <token>
```
### Protected Routes
- `/api/user/*` — user management
- `/api/group/*` — group management
- `/api/token/*` — token management
- `/api/notification/*` — notifications
- `/api/oauth/client/*` — OAuth client management
### Permission Groups
| Group | Grants |
|-------|--------|
| `app_sso_admin` | Full user/group/notification management |
| `app_sso_oauth_admin` | Register and manage OAuth clients |
| `app_sso_invite` | Invitation management |
| Group owner | Manage membership of that specific group |
Self-service: users can always read and modify their own account without admin membership.
### Rate Limits
| Endpoint | Limit |
|----------|-------|
| `POST /api/auth/login` | 10 requests / 15 min |
| `POST /api/auth/resetpassword` | 5 requests / hour |
| `POST /api/auth/otp/request` | 5 requests / 15 min |
| `POST /api/auth/otp/verify` | 10 requests / 15 min |
| `POST /api/auth/invite/*` | 20 requests / hour |
Limits are per IP. The server must be behind a trusted proxy for these to apply correctly.
---
## Authentication Endpoints
Base path: `/api/auth`
### Login
**`POST /api/auth/login`** — No auth required
**Request:**
```json
{ "uid": "username", "password": "user_password" }
```
**Response:**
```json
{ "login": true, "token": "auth_token_string", "message": "username logged in!" }
```
---
### Logout
**`ALL /api/auth/logout`** — No auth required
**Response:**
```json
{ "message": "Bye" }
```
---
### Username Suggestions
Returns available username suggestions based on name and optional date of birth.
**`GET /api/auth/username-suggestions`** — No auth required
**Query Parameters:**
- `givenName` — First name
- `sn` — Last name
- `dob` (optional) — Date of birth (used to generate year-variant suggestions)
**Response:**
```json
{ "suggestions": ["jsmith", "jsmith1990", "johnsmith"] }
```
---
### Request Password Reset
**`POST /api/auth/resetpassword`** — No auth required
**Request:**
```json
{ "mail": "user@example.com" }
```
**Response:**
```json
{ "message": "If the email address is in our system, you will receive a message." }
```
The same response is returned whether or not the email exists. Reset tokens expire after 24 hours.
---
### Complete Password Reset
**`POST /api/auth/resetpassword/:token`** — No auth required
**URL Parameters:** `token` — from the reset email
**Request:**
```json
{ "password": "new_password", "confirm": "new_password" }
```
**Response:**
```json
{ "message": "Password has been changed." }
```
The token is invalidated after a successful reset.
---
### Request OTP Code
Send a one-time login code via email or SMS.
**`POST /api/auth/otp/request`** — No auth required
**Request:**
```json
{ "login": "username_or_email", "method": "email" }
```
`method` must be `email` or `sms`. For `sms`, the account must have a mobile number on file.
**Response:**
```json
{ "message": "Code sent", "method": "email", "expires_at": 1234567890000 }
```
---
### Verify OTP Code
Verify a one-time code and receive an auth token. Also marks the corresponding contact method (email or phone) as verified on the account.
**`POST /api/auth/otp/verify`** — No auth required
**Request:**
```json
{ "login": "username_or_email", "code": "123456" }
```
**Response:**
```json
{ "login": true, "token": "auth_token_string" }
```
Returns `401` if the code is invalid or expired.
---
### Accept Invite (with Email Verification)
Create an account from an invite token after verifying email.
**`POST /api/auth/invite/:token/:mailToken`** — No auth required
**URL Parameters:** `token` — invite token, `mailToken` — email verification token
**Request:**
```json
{
"uid": "username",
"password": "user_password",
"givenName": "First Name",
"sn": "Last Name",
"mail": "user@example.com"
}
```
**Response:**
```json
{ "user": "username", "token": "auth_token_string" }
```
---
### Send Invite Verification Email
Send an email verification link for an invite token.
**`POST /api/auth/invite/:token`** — No auth required
**URL Parameters:** `token` — invite token
**Request:**
```json
{ "mail": "user@example.com" }
```
**Response:**
```json
{ "message": "sent" }
```
---
### Impersonate User (Admin)
Creates a temporary password for a target user and returns it. Allows admins to log in as another user for support purposes.
**`POST /api/auth/impersonate/:uid`** — Auth required, `app_sso_admin`
**URL Parameters:** `uid` — target username
**Response:**
```json
{
"uid": "target_username",
"temp_password": "random_temp_password",
"expires_at": 1234567890000
}
```
The temp password is short-lived. Any previous impersonation session for the same target is revoked first.
---
### End Impersonation (Admin)
Revoke all active impersonation sessions for a user.
**`DELETE /api/auth/impersonate/:uid`** — Auth required, `app_sso_admin`
**URL Parameters:** `uid` — target username
**Response:**
```json
{ "message": "Impersonation ended for username", "revoked": 1 }
```
---
## User Management Endpoints
Base path: `/api/user`
All endpoints require authentication.
### List All Users
**`GET /api/user/`** — `app_sso_admin` required
**Query Parameters:**
- `detail` (optional) — return full user objects instead of a minimal list
**Response:**
```json
{
"results": [
{ "uid": "username", "mail": "user@example.com", "givenName": "First", "sn": "Last" }
]
}
```
---
### Create User
**`POST /api/user/`** — `app_sso_admin` required
**Request:**
```json
{
"uid": "username",
"password": "user_password",
"givenName": "First Name",
"sn": "Last Name",
"mail": "user@example.com"
}
```
**Response:**
```json
{ "results": { "uid": "username", "dn": "uid=username,ou=people,dc=..." } }
```
Admin-created accounts have `password_must_change` set automatically.
---
### Get Current User
**`GET /api/user/me`** — Any authenticated user
Returns the full user object for the authenticated user, including onboarding state.
**Response:**
```json
{
"uid": "username",
"mail": "user@example.com",
"givenName": "First",
"sn": "Last",
"dn": "uid=username,ou=people,dc=...",
"onboardingRequired": "yes",
"onboardingNeeds": ["tos", "dob", "password"],
"memberOf": ["cn=group_name,ou=groups,dc=..."]
}
```
`onboardingRequired` is `"yes"` when `onboardingNeeds` is non-empty. Needs can be `"tos"`, `"dob"`, or `"password"`.
---
### Accept Terms of Service
Mark TOS as accepted for the authenticated user.
**`POST /api/user/accept-tos`** — Any authenticated user
**Response:**
```json
{ "success": true }
```
---
### Get Admin Stats
Returns user and group counts, recent signups, and inactive users.
**`GET /api/user/stats`** — `app_sso_admin` required
**Response:**
```json
{
"totalUsers": 42,
"activeUsers": 38,
"inactiveUsers": 4,
"totalGroups": 10,
"recentSignups": [
{ "uid": "newuser", "givenName": "New", "sn": "User", "mail": "new@example.com", "createTimestamp": "20240101000000Z" }
],
"inactiveList": [
{ "uid": "lockeduser", "givenName": "Locked", "sn": "User", "mail": "locked@example.com" }
]
}
```
---
### Export Users CSV
Download all users as a CSV file.
**`GET /api/user/export`** — `app_sso_admin` required
**Response:** `Content-Type: text/csv`, attachment download
Columns: `uid`, `givenName`, `sn`, `mail`, `mobile`, `uidNumber`, `isActive`, `createTimestamp`
---
### Get User Verification Status
**`GET /api/user/:uid/verification`** — `app_sso_admin` required
**Response:**
```json
{
"uid": "username",
"emailVerified": true,
"emailVerifiedAt": 1234567890000,
"phoneVerified": false,
"phoneVerifiedAt": null,
"tosAccepted": true,
"tosAcceptedAt": 1234567890000
}
```
---
### Get User
**`GET /api/user/:uid`** — Any authenticated user
**Response:**
```json
{ "results": { "uid": "username", "mail": "user@example.com", "givenName": "First", "sn": "Last" } }
```
---
### Update User
**`PUT /api/user/:uid`** — Own account, or `app_sso_admin` for others
**Request:** Any subset of editable fields:
```json
{
"givenName": "New First Name",
"sn": "New Last Name",
"mail": "newemail@example.com",
"dateOfBirth": "1990-01-15",
"mobile": "+15551234567",
"sshPublicKey": "ssh-rsa AAAA..."
}
```
**Response:**
```json
{ "results": { <updated user object> }, "message": "Updated username user" }
```
---
### Set User Active/Inactive
Lock or unlock a user account.
**`PUT /api/user/:uid/active`** — `app_sso_admin` required
**Request:**
```json
{ "active": true }
```
**Response:**
```json
{ "uid": "username", "active": true, "message": "User username activated" }
```
---
### Delete User
**`DELETE /api/user/:uid`** — Own account, or `app_sso_admin` for others
**Response:**
```json
{ "uid": "username", "results": true }
```
---
### Change Own Password
**`PUT /api/user/password`** — Any authenticated user
**Request:**
```json
{ "password": "new_password", "confirm": "new_password" }
```
**Response:**
```json
{ "results": true }
```
Clears `password_must_change` on the account.
---
### Change User Password (Admin)
**`PUT /api/user/:uid/password`** — Own account, or `app_sso_admin` for others
**Request:**
```json
{ "password": "new_password", "confirm": "new_password" }
```
**Response:**
```json
{ "results": true, "message": "User username password changed." }
```
When an admin changes another user's password, `password_must_change` is set on that account.
---
### Generate Invite Token
Create an invite token, optionally emailing it to the invitee and pre-assigning
LDAP groups the new account should be added to on signup.
**`POST /api/user/invite`** — `app_sso_admin` or `app_sso_invite` required
**Request:**
```json
{ "mail": "invitee@example.com", "groups": ["group_name"] }
```
`mail` and `groups` are both optional. If `mail` is provided, a verification
email is sent to that address.
**Response:**
```json
{
"token": "invite_token_string",
"link": "https://your-domain.com/login/invite/invite_token_string",
"mail_sent": true
}
```
---
### List Invite Tokens
**`GET /api/user/invite`** — `app_sso_admin` or `app_sso_invite` required
Admins (`app_sso_admin`) see all invite tokens; non-admin `app_sso_invite`
members see only invites they created.
**Response:**
```json
{
"results": [
{
"token": "invite_token_string",
"created_by": "username",
"created_on": 1234567890000,
"is_valid": true,
"mail": "invitee@example.com",
"groups": "[\"group_name\"]"
}
]
}
```
---
### Update Invite Token
Update the groups an invite will assign, or change/clear the invitee's email
(re-sends verification if a new email is set).
**`PUT /api/user/invite/:token`** — `app_sso_admin`, or the `app_sso_invite`
member who created the invite
**URL Parameters:** `token` — invite token
**Request:** Any subset of:
```json
{ "groups": ["group_name"], "mail": "newinvitee@example.com" }
```
Set `mail` to `null`/empty to clear it. Fails with `400` if the token is no
longer valid.
**Response:**
```json
{ "results": { "token": "invite_token_string", "is_valid": true, "...": "..." } }
```
---
### Revoke Invite Token
**`DELETE /api/user/invite/:token`** — `app_sso_admin`, or the `app_sso_invite`
member who created the invite
**URL Parameters:** `token` — invite token
**Response:**
```json
{ "results": true }
```
---
### Add SSH Key
**`POST /api/user/key`** — Any authenticated user
**Request:**
```json
{ "key": "ssh-rsa AAAAB3NzaC1yc2E... user@host" }
```
**Response:**
- `200``{ "message": true }`
- `400``{ "message": "error description" }`
---
## Group Management Endpoints
Base path: `/api/group`
All endpoints require authentication.
### List Groups
**`GET /api/group/`** — Any authenticated user
**Query Parameters:**
- `detail` (optional) — return full group objects
- `member` (optional) — filter to groups containing this UID as a member
**Response:**
```json
{ "results": [{ "cn": "group_name", "description": "Group description" }] }
```
---
### Create Group
**`POST /api/group/`** — `app_sso_admin` required
**Request:**
```json
{ "name": "group_name", "description": "Group description" }
```
**Response:**
```json
{ "results": { "cn": "group_name" }, "message": "group_name was added!" }
```
The authenticated user is automatically set as the group owner.
---
### Get Group
**`GET /api/group/:name`** — Any authenticated user
**Response:**
```json
{
"results": {
"cn": "group_name",
"description": "Group description",
"member": ["uid=user1,ou=people,dc=...", "uid=user2,ou=people,dc=..."],
"owner": ["uid=owner,ou=people,dc=..."]
}
}
```
---
### Add Group Owner
**`PUT /api/group/owner/:group/:uid`** — `app_sso_admin` or group owner
**Response:**
```json
{ "results": true, "message": "Added owner uid to group group." }
```
---
### Remove Group Owner
**`DELETE /api/group/owner/:group/:uid`** — `app_sso_admin` or group owner
**Response:**
```json
{ "results": true, "message": "Removed Owner uid from group group." }
```
---
### Add User to Group
**`PUT /api/group/:group/:uid`** — `app_sso_admin` or group owner
**Response:**
```json
{ "results": true, "message": "Added user uid to group group." }
```
---
### Remove User from Group
**`DELETE /api/group/:group/:uid`** — `app_sso_admin` or group owner
**Response:**
```json
{ "results": true, "message": "Removed user uid from group group." }
```
---
### Delete Group
**`DELETE /api/group/:group`** — `app_sso_admin` or group owner
**Response:**
```json
{ "removed": true, "results": { "cn": "group_name" }, "message": "Group group_name Deleted" }
```
---
## Token Management Endpoints
Base path: `/api/token`
All endpoints require authentication.
### List Token Types
**`GET /api/token/`** — Any authenticated user
**Response:**
```json
{ "results": ["InviteToken", "PasswordResetToken"] }
```
---
### List Tokens by Type
**`GET /api/token/:name`** — Any authenticated user
**Query Parameters:** `detail` (optional) — include full token objects
**Response:**
```json
{
"results": [
{ "token": "token_string", "created_by": "username", "created_on": 1234567890000, "is_valid": true }
]
}
```
---
### Get Specific Token
**`GET /api/token/:name/:token`** — Any authenticated user
**Response:**
```json
{ "results": { "token": "token_string", "created_by": "username", "created_on": 1234567890000, "is_valid": true } }
```
---
## Notification Endpoints
Base path: `/api/notification`
All endpoints require authentication and `app_sso_admin` membership.
Notifications send email blasts to filtered groups of users and record history.
### Send Notification
**`POST /api/notification`** — `app_sso_admin` required
**Request:**
```json
{
"subject": "Maintenance window tonight",
"message": "<p>We will be performing maintenance starting at midnight.</p>",
"filter_type": "group",
"filter_value": "host_hec-bot_admin, app_sso_admin",
"active_only": true
}
```
**`filter_type` values:**
| Value | Description |
|-------|-------------|
| `all_active` | All active (unlocked) users |
| `all` | All users including inactive |
| `group` | Members of one or more LDAP groups |
| `users` | Specific users by UID |
**`filter_value`:**
- For `group`: comma-separated group names (e.g. `"host_hec-bot_admin, app_sso_admin"`)
- For `users`: JSON array of UIDs (e.g. `'["wmantly","jsmith"]'`)
- Unused for `all_active` and `all`
**`active_only`** (boolean, default `false`): when `true` with `group` or `all` filter types, only active (unlocked) users receive the notification. Has no effect on `all_active` (which is always active-only).
**Response:**
```json
{
"results": {
"notification_id": "uuid",
"created_by": "wmantly",
"created_on": 1234567890000,
"subject": "Maintenance window tonight",
"message": "<p>...</p>",
"filter_type": "group",
"filter_value": "host_hec-bot_admin",
"active_only": true,
"status": "sent",
"sent_count": 5,
"failed_count": 0,
"sent_at": 1234567890123
}
}
```
**External API usage example:**
```bash
# Login once, store token
TOKEN=$(curl -s -X POST https://sso.example.com/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"uid":"monitor","password":"..."}' | jq -r .token)
# Send notification to a group
curl -X POST https://sso.example.com/api/notification \
-H "auth-token: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"subject": "hec-bot maintenance",
"message": "<p>The hec-bot VM will restart at 3am.</p>",
"filter_type": "group",
"filter_value": "host_hec-bot_admin",
"active_only": true
}'
```
---
### List Notification History
**`GET /api/notification`** — `app_sso_admin` required
**Response:**
```json
{
"results": [
{
"notification_id": "uuid",
"subject": "Maintenance window",
"filter_type": "group",
"filter_value": "host_hec-bot_admin",
"active_only": true,
"status": "sent",
"sent_count": 5,
"failed_count": 0,
"created_by": "wmantly",
"created_on": 1234567890000,
"sent_at": 1234567890123
}
]
}
```
Results are sorted by `created_on` descending (most recent first).
---
### Get Notification Record
**`GET /api/notification/:id`** — `app_sso_admin` required
**Response:** `{ "results": { <notification object> } }`
---
## OAuth 2.0 / OpenID Connect
The SSO Manager acts as an OAuth 2.0 Authorization Server and OpenID Connect Provider.
### Discovery
**`GET /.well-known/openid-configuration`** — No auth required
Returns the OIDC discovery document with endpoint URLs, supported scopes, and signing algorithms.
---
### Authorization Endpoint
**`GET /oauth/authorize`** — No auth required (redirects to login if not authenticated)
**Query Parameters:**
- `response_type` — Must be `code`
- `client_id` — Registered OAuth client ID
- `redirect_uri` — Must match a URI registered for the client, either exactly
or against a registered wildcard pattern (`*` = one hostname label, `**` =
any number of labels)
- `scope` — Space-separated: `openid`, `profile`, `email`
- `state` — Opaque value returned unchanged in the redirect
- `code_challenge` — PKCE challenge (SHA-256 of code_verifier, base64url-encoded)
- `code_challenge_method` — Must be `S256`
Renders the consent screen.
---
### Authorize (Issue Code)
Issues the authorization code after the user approves the consent form shown
by the Authorization Endpoint above. This is called by the consent page itself
(an authenticated request, via `auth-token`), not by the OAuth client
directly.
**`POST /api/oauth/authorize`** — Auth required (`auth-token` header)
**Request:**
```json
{
"response_type": "code",
"client_id": "uuid",
"redirect_uri": "https://ha.example.com/auth/external/callback",
"scope": "openid profile email",
"state": "opaque-state-value",
"code_challenge": "pkce-challenge",
"code_challenge_method": "S256"
}
```
Only scopes the client is actually registered for are granted, even if more
are requested. If the client has `allowed_groups` set, the authenticated user
must be a member of at least one of those groups or the request is rejected
with `403`.
**Response:**
```json
{ "redirect_url": "https://ha.example.com/auth/external/callback?code=<code>&state=<state>" }
```
The caller (the consent page) redirects the browser to `redirect_url`, which
completes the flow described in the Authorization Endpoint section above.
---
### Token Endpoint
**`POST /oauth/token`** — No auth required (client authenticates via credentials)
**Content-Type:** `application/x-www-form-urlencoded` or `application/json`
**Client Authentication:** `client_id` + `client_secret` in the request body, or HTTP Basic Auth.
#### Authorization Code Grant
```
grant_type=authorization_code
&code=<auth_code>
&redirect_uri=<redirect_uri>
&client_id=<client_id>
&client_secret=<client_secret>
&code_verifier=<pkce_verifier>
```
#### Refresh Token Grant
```
grant_type=refresh_token
&refresh_token=<refresh_token>
&client_id=<client_id>
&client_secret=<client_secret>
```
Refresh tokens are rotated on each use — the old token is invalidated and a new one is returned.
**Response:**
```json
{
"access_token": "uuid",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "uuid",
"id_token": "<jwt>"
}
```
---
### UserInfo Endpoint
**`GET /oauth/userinfo`** — Bearer token required (`Authorization: Bearer <access_token>`)
**Response** (claims vary by granted scopes):
```json
{
"sub": "username",
"name": "First Last",
"given_name": "First",
"family_name": "Last",
"preferred_username": "username",
"email": "user@example.com"
}
```
---
## OAuth Client Management
Base path: `/api/oauth/client`
All endpoints require authentication and `app_sso_oauth_admin` membership.
### List Clients
**`GET /api/oauth/client/`**
**Response:**
```json
{
"results": [
{
"client_id": "uuid",
"name": "Home Assistant",
"description": "Home automation",
"redirect_uris": ["https://ha.example.com/auth/external/callback"],
"scopes": ["openid", "profile", "email"],
"allowed_groups": [],
"token_lifetime": { "access_token": 3600, "refresh_token": 2592000 },
"created_by": "wmantly",
"created_on": 1234567890000
}
]
}
```
---
### Register Client
**`POST /api/oauth/client/`**
**Request:**
```json
{
"name": "Home Assistant",
"description": "Home automation dashboard",
"redirect_uris": ["https://ha.example.com/auth/external/callback"],
"scopes": ["openid", "profile", "email"],
"allowed_groups": [],
"token_lifetime": { "access_token": 3600, "refresh_token": 2592000 }
}
```
`redirect_uris` may also be a newline-separated string. `scopes` may also be a space-separated string.
`allowed_groups` restricts the client to members of the listed SSO groups (empty/omitted = any valid user).
**Response:**
```json
{
"results": { "client_id": "uuid", "name": "Home Assistant" },
"client_secret": "raw-secret-shown-once",
"message": "OAuth client 'Home Assistant' created. Save the client secret — it will not be shown again."
}
```
The `client_secret` is shown **only once**. Store it immediately.
---
### Get Client
**`GET /api/oauth/client/:client_id`**
**Response:** `{ "results": { <client object> } }`
---
### Update Client
**`PUT /api/oauth/client/:client_id`**
**Request:** Any subset of `name`, `description`, `redirect_uris`, `scopes`, `allowed_groups`, `token_lifetime`, `is_valid`.
**Response:** `{ "results": { <updated client> }, "message": "..." }`
---
### Delete Client
**`DELETE /api/oauth/client/:client_id`**
**Response:** `{ "client_id": "uuid", "message": "OAuth client '...' deleted." }`
---
### Rotate Client Secret
**`POST /api/oauth/client/:client_id/rotate`**
**Response:**
```json
{
"client_secret": "new-raw-secret-shown-once",
"message": "Client secret rotated for '...'. Save it — it will not be shown again."
}
```
The old secret is invalidated immediately. The new secret is shown **only once**.
---
### Token Lifetimes
Configurable per-client via `token_lifetime`. Global defaults (in seconds):
```json
{ "access_token": 3600, "refresh_token": 2592000 }
```
---
## Error Responses
All endpoints return errors in this format:
```json
{ "name": "ErrorName", "message": "Error message description" }
```
### HTTP Status Codes
| Code | Meaning |
|------|---------|
| `200` | Success |
| `400` | Bad Request — invalid input |
| `401` | Unauthorized — auth required, token invalid, or insufficient permission |
| `404` | Not Found |
| `429` | Too Many Requests — rate limit exceeded |
| `500` | Internal Server Error |
| `502` | Bad Gateway — upstream service failure (e.g. SMS delivery) |