Appearance
Authentication
The Layota REST API uses JWT tokens for user authentication and API keys for server-to-server access.
JWT Authentication
Register
http
POST /api/auth/register/
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword",
"password_confirm": "securepassword",
"full_name": "John Doe",
"organization_name": "My Company"
}Response:
json
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"fullName": "John Doe",
"date_joined": "2026-04-12T10:00:00Z",
"avatarUrl": null
},
"tokens": {
"access": "eyJhbGci...",
"refresh": "eyJhbGci..."
}
}Login
http
POST /api/auth/login/
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword"
}Response:
json
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"fullName": "John Doe",
"date_joined": "2026-04-12T10:00:00Z",
"avatarUrl": "https://...",
"emailVerified": true,
"twoFactorEnabled": false,
"hasPassword": true
},
"tokens": {
"access": "eyJhbGci...",
"refresh": "eyJhbGci..."
}
}If the account has two-factor authentication enabled, login (and Google sign-in) does not return tokens. Instead it returns a challenge — complete it via /auth/2fa/login-verify/:
json
{
"two_factor_required": true,
"pending_token": "g7c2..."
}Sign in with Google
http
POST /api/auth/google/
Content-Type: application/json
{
"code": "4/0AeanS0b...",
"code_verifier": "dBjftJeZ4CVP..."
}Exchanges a Google OAuth 2.0 authorization code for a Layota session. The app uses the standard authorization-code redirect flow with state and PKCE (S256): the current tab navigates to Google's auth endpoint (no prompt parameter, so returning users skip the consent screen), Google redirects back to the SPA's /auth/google/callback, and that page POSTs the one-time code plus the PKCE code_verifier here. The backend rebuilds the exchange redirect_uri from the request's Origin header (<origin>/auth/google/callback — each environment's origin must be registered as an Authorized redirect URI on the Google OAuth client), exchanges the code with Google, verifies the returned ID token (signature, audience, issuer, expiry), and then:
- matches a returning user by their stable Google account id (
sub), otherwise - links to an existing account with the same verified email, otherwise
- creates a new account (email pre-verified, no password, personal organization created automatically).
On success the response is identical to Login:
json
{
"user": { "id": "...", "email": "user@example.com", "fullName": "John Doe", "hasPassword": false },
"tokens": { "access": "eyJhbGci...", "refresh": "eyJhbGci..." }
}Notable responses:
400— missingcode/code_verifier, or noOriginheader on the request.401— Google authentication failed, invalid token, or unverified Google email.403— the user's organization membership is blocked.503— Google sign-in is not configured on this server (GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETunset).
The user.hasPassword field is false for accounts that only ever signed in with Google (no usable password). Such users can set a password via the Password Reset flow to additionally enable email/password login.
Two-Factor Authentication (2FA)
2FA is TOTP-based (RFC 6238) — compatible with Google Authenticator, Authy, 1Password, etc. When enabled, the user must supply a 6-digit code (or a one-time backup code) after their password.
Enrollment (authenticated, two steps):
http
POST /api/auth/2fa/setup/
Authorization: Bearer <access>Returns a provisional secret and the otpauth:// URI to render as a QR code. It is not active until confirmed:
json
{
"secret": "JBSWY3DPEHPK3PXP",
"otpauth_uri": "otpauth://totp/Layota:user@example.com?secret=...&issuer=Layota"
}http
POST /api/auth/2fa/enable/
Authorization: Bearer <access>
{ "code": "123456" }Confirms a code against the provisional secret, switches 2FA on, and returns one-time backup codes (shown only once):
json
{ "enabled": true, "backup_codes": ["4d6b4-5ff04", "2cddc-634ad", "..."] }Login challenge — when a 2FA user signs in, /auth/login/ (or /auth/google/) returns { "two_factor_required": true, "pending_token": "..." } instead of tokens. Exchange the pending token + a code for a real session:
http
POST /api/auth/2fa/login-verify/
{ "pending_token": "g7c2...", "code": "123456" }code accepts either the current TOTP or one of the user's backup codes (each backup code works once). On success the response is identical to Login. The pending token expires after 5 minutes and is burned after 5 wrong attempts.
Disable (authenticated) — requires a current code so a hijacked session can't silently remove the second factor:
http
POST /api/auth/2fa/disable/
Authorization: Bearer <access>
{ "code": "123456" }Notable responses:
400— wrong/expired code, or enrollment not started.401— wrong code at the login challenge.429— too many attempts (throttled).
Refresh Token
http
POST /api/auth/token/refresh/
Content-Type: application/json
{
"refresh": "eyJhbGci..."
}Response:
json
{
"access": "eyJhbGci..."
}Logout
http
POST /api/auth/logout/
Content-Type: application/json
{
"refresh": "eyJhbGci..."
}Blacklists the provided refresh token so it can no longer be exchanged for new access tokens. Always returns 200 OK — missing / malformed / already-blacklisted tokens are treated as success so clients can safely clear local state in all cases.
json
{
"detail": "Logged out."
}Using the Token
Include the access token in the Authorization header:
http
GET /api/projects/
Authorization: Bearer eyJhbGci...Get Current User
http
GET /api/auth/me/
Authorization: Bearer eyJhbGci...Response:
json
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"fullName": "John Doe",
"date_joined": "2026-04-12T10:00:00Z",
"avatarUrl": "https://...",
"emailVerified": true,
"twoFactorEnabled": false,
"hasPassword": true
}
}Update Profile
http
PATCH /api/auth/me/
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"fullName": "Jane Doe"
}Also accepts PUT for full replacement.
Delete Account
http
DELETE /api/auth/me/
Authorization: Bearer eyJhbGci...Permanently deletes the user account and all associated data. If the user is the sole owner of an organization, the organization and all its projects are also deleted.
Upload Avatar
http
POST /api/auth/me/avatar/
Authorization: Bearer eyJhbGci...
Content-Type: multipart/form-data
avatar=<file>Delete Avatar
http
DELETE /api/auth/me/avatar/
Authorization: Bearer eyJhbGci...Change Password
http
POST /api/auth/change-password/
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"current_password": "oldpassword",
"new_password": "newpassword",
"new_password_confirm": "newpassword"
}Response:
json
{
"message": "Password changed successfully."
}Set Password
For accounts created via Sign in with Google that have no password yet (user.hasPassword is false). Adds email/password login alongside Google. Unlike Change Password it does not require a current password, and it only works while the account is still passwordless.
http
POST /api/auth/set-password/
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"new_password": "newpassword",
"new_password_confirm": "newpassword"
}Response (the returned user now has hasPassword: true):
json
{
"message": "Password set successfully.",
"user": { "id": "...", "email": "user@example.com", "hasPassword": true }
}Returns 400 if the account already has a password (use Change Password instead). The current session is not invalidated — there is no old credential to rotate.
Password Reset
Request a reset link:
http
POST /api/auth/password-reset/
Content-Type: application/json
{
"email": "user@example.com"
}Confirm the reset:
http
POST /api/auth/password-reset/confirm/
Content-Type: application/json
{
"uid": "...",
"token": "...",
"new_password": "newpassword",
"new_password_confirm": "newpassword"
}Response:
json
{
"detail": "Password has been reset successfully."
}API Key Authentication
For server-to-server access, use API keys instead of JWT tokens. See API Keys.
http
GET /api/projects/
Authorization: Bearer sk_...Error Responses
400 Bad Request
json
{
"email": ["This field is required."],
"password": ["This password is too common."]
}401 Unauthorized
json
{
"detail": "Authentication credentials were not provided."
}403 Forbidden
json
{
"detail": "You do not have permission to perform this action."
}