Auth — /v1/auth
All auth endpoints live at /v1/auth/. None require an Authorization header unless marked requires session.
POST /v1/auth/register
Section titled “POST /v1/auth/register”Create a new user account and organization.
Request
{ "org_name": "Acme AI", "email": "you@acme.com", "password": "min-8-chars"}Response 201
{ "user": { "user_id": "usr_abc123", "email": "you@acme.com", "email_verified": false, "org_id": "org_xyz789" }}Errors
| Status | Error | Reason |
|---|---|---|
| 400 | validation_error | Missing or invalid fields |
| 409 | — | Email already registered |
A verification email is sent automatically.
POST /v1/auth/login
Section titled “POST /v1/auth/login”Exchange email + password for a signed JWT (valid 24h).
Request
{ "email": "you@acme.com", "password": "your-password"}Response 200
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "user_id": "usr_abc123", "email": "you@acme.com", "email_verified": true, "org_id": "org_xyz789" }}Errors
| Status | Reason |
|---|---|
| 400 | Validation error |
| 401 | Invalid credentials |
POST /v1/auth/logout
Section titled “POST /v1/auth/logout”Requires session. Signals logout — JWT is stateless so the client must discard the token.
Response 200
{ "success": true }GET /v1/auth/me
Section titled “GET /v1/auth/me”Requires session. Returns the current user and their organization.
Response 200
{ "user": { "user_id": "usr_abc123", "email": "you@acme.com", "email_verified": true, "org_id": "org_xyz789", "created_at": "2026-05-01T12:00:00Z" }, "org": { "org_id": "org_xyz789", "name": "Acme AI", "plan": "starter" }}POST /v1/auth/verify-email
Section titled “POST /v1/auth/verify-email”Confirm an email address using the token from the verification email.
Request
{ "token": "<verification-token>" }Response 200
{ "success": true }Errors
| Status | Reason |
|---|---|
| 400 | Token missing or malformed |
| 400 | Token expired or already used |
POST /v1/auth/resend-verification
Section titled “POST /v1/auth/resend-verification”Re-send the email verification link.
Request
{ "email": "you@acme.com" }Response 200
{ "success": true }Always returns 200 to prevent email enumeration.
POST /v1/auth/forgot-password
Section titled “POST /v1/auth/forgot-password”Send a password reset link to the given email address.
Request
{ "email": "you@acme.com" }Response 200
{ "success": true }Always returns 200 even if the email doesn’t exist (prevents enumeration). Reset links expire after 1 hour.
POST /v1/auth/reset-password
Section titled “POST /v1/auth/reset-password”Set a new password using a valid reset token.
Request
{ "token": "<reset-token-from-email>", "password": "new-password-min-8"}Response 200
{ "success": true }Errors
| Status | Reason |
|---|---|
| 400 | Validation error |
| 400 | Token expired or already used |
Session JWT format
Section titled “Session JWT format”The JWT payload contains:
{ "user_id": "usr_abc123", "email": "you@acme.com", "org_id": "org_xyz789", "iat": 1748390400, "exp": 1748476800}Algorithm: HS256. Expiry: 24 hours.
Pass the token as a Bearer header on session-protected endpoints:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...The same Authorization: Bearer header also accepts ctv_key_… API keys on all non-auth routes — the backend distinguishes by prefix.