Authentication
The BottleCRM API uses JWT access + refresh tokens signed by the backend. Every request to a protected endpoint must carry the access token in the Authorization header.
Obtaining tokens
There are three production paths to a token pair, plus one local-only shortcut.
Google OAuth
The frontend sends the Google ID token it received from the browser:
POST /api/auth/google/
Content-Type: application/json
{ "id_token": "eyJ…" }
Response:
{
"access": "eyJhbGc…",
"refresh": "eyJhbGc…",
"user": { "id": "…", "email": "…" },
"organizations": [{ "id": "…", "name": "Acme" }]
}
If the user belongs to a single org, the access token already contains the org_id claim. Otherwise the frontend calls POST /api/auth/switch-org/ with the chosen org ID and gets a re-signed pair.
Magic links
POST /api/auth/magic-link/request/
{ "email": "user@example.com" }
The user receives an email with a one-time link. The link's token is exchanged for JWTs:
POST /api/auth/magic-link/verify/
{ "token": "…" }
Dev login (local only)
When DEBUG=True, the management command manage.py devlogin <email> --org <name> prints a ready-to-use token pair. The command refuses to run in production.
Using a token
GET /api/leads/
Authorization: Bearer eyJhbGc…
The token carries the user's profile_id and the active org_id. The backend resolves the profile on every request and sets app.current_org for row-level security.
Refreshing
Access tokens are short-lived (default 15 minutes). Use the refresh token to mint a new access token:
POST /api/auth/refresh/
{ "refresh": "eyJhbGc…" }
Refresh tokens themselves are rotated on use; the response contains a new refresh token that supersedes the old one.
Switching organizations
A user with profiles in multiple orgs can swap tenants without re-authenticating:
POST /api/auth/switch-org/
Authorization: Bearer <current access token>
{ "org_id": "ab12-cd34-…" }
The response is a fresh access + refresh pair with the new org_id baked in. The previous pair becomes invalid for org-scoped queries.
Logging out
The client just discards its tokens. Refresh tokens are server-tracked, so the next refresh attempt with the old token will fail.
To revoke all sessions for a user (e.g. compromised account), rotate JWT_SIGNING_KEY in the backend's environment.