POST /v1/auth/register
Create a new organization and its owner user in a single atomic operation. Returns a fully-issued browser session on success.
::: tip Auth Required: none. This endpoint is intentionally unauthenticated — it's the on-ramp for new tenants. :::
Request
POST /v1/auth/register
| Header | Required | Notes |
|---|---|---|
Content-Type: application/json | yes | — |
Body
{
"email": "alice@example.com",
"password": "correct-horse-battery-staple",
"display_name": "Alice Anderson",
"organization_name": "Acme Co.",
"organization_slug": "acme-co"
}
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Valid email format. Must be globally unique. |
password | string | yes | Minimum 12 characters, max 256. |
display_name | string | null | no | Shown in the UI; defaults to email if not set. |
organization_name | string | yes | 1–100 chars. Human-readable name. |
organization_slug | string | null | no | URL-safe identifier. If omitted, derived from the name. Must match [a-z0-9-]{1,100}. |
Response
201 Created
A full LoginResponse::Success body, plus session cookies on the response:
{
"status": "success",
"user": {
"user_id": "1c8b2a5e-...",
"organization_id": "ec3f7b1a-...",
"email": "alice@example.com",
"is_platform_admin": false,
"platform_role": null,
"api_key_id": null,
"api_key_prefix": null,
"api_key_name": null,
"team_id": null,
"project_id": null,
"auth_method": "password",
"session_family_id": "9a4d3c1e-...",
"mfa_verified_at": null,
"scopes": [],
"allowed_ips": [],
"allowed_referrers": [],
"extensions": {}
},
"capabilities": {
"role": "owner",
"permissions": ["org.read", "members.invite", "members.remove", "..."]
},
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 900
}
Cookies set:
identsphere_at— access token,HttpOnly, scope/identsphere_rt— refresh token,HttpOnly, scope/v1/authidentsphere_csrf— CSRF token, scope/, NOTHttpOnly
Error responses
| Status | Code | When |
|---|---|---|
| 400 | invalid_input | Email format invalid, password too short, slug invalid format. |
| 409 | conflict | email already registered or organization slug already taken. |
| 500 | internal_error | Database, hashing, or JWT failure. |
Example: curl
curl -X POST https://auth.example.com/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{
"email": "alice@example.com",
"password": "correct-horse-battery-staple",
"organization_name": "Acme Co."
}'
Example: TypeScript (@identsphere/react)
import { useRegister } from '@identsphere/react';
function SignupForm() {
const register = useRegister();
return (
<button
onClick={() =>
register.mutateAsync({
email: 'alice@example.com',
password: 'correct-horse-battery-staple',
organization_name: 'Acme Co.',
})
}
>
Sign up
</button>
);
}
Notes
- This endpoint always creates a new organization. To invite an existing user
into an existing organization, use
POST /v1/orgs/:org_id/invitations. - The new user is assigned the
ownerrole on the new org. - A registration audit entry (
auth.register) is recorded asynchronously. - The response is identical in shape to a successful
POST /v1/auth/login— client code can share a single handler.