User Management API Documentation
Overview
The User Management API provides complete CRUD operations for managing users within your multi-tenant Django application. Each tenant has its own isolated user base, and this API allows you to create, read, update, and delete users within the current tenant's schema.
Base URL: /api/users/
Authentication: JWT Bearer Token (required for all endpoints)
Permissions:
- All endpoints require authentication
- Non-superusers can only see active users
- Non-superusers cannot delete superusers
- Users cannot delete their own accounts
API Response Format
All API responses follow a consistent wrapper format using AppResponse and AppDataResponse classes:
Success Responses (with data)
Endpoints that return data (GET, POST, PUT) wrap the response in this format:
{
"success": true,
"message": "Operation description",
"status_code": 200,
"data": {
// Actual response data here
}
}
Success Responses (without data)
Endpoints that don't return data (DELETE) use this format:
{
"success": true,
"message": "Operation completed successfully",
"status_code": 200
}
Paginated Responses
List endpoints that use pagination wrap the results in:
{
"success": true,
"message": "Data retrieved successfully",
"status_code": 200,
"data": [
// Array of items
],
"total": 100,
"page": 1,
"page_size": 10,
"total_pages": 10
}
Error Responses
Error responses follow this format:
{
"success": false,
"message": "Error description",
"status_code": 400
}
Validation Error Responses
Validation errors include field-level errors:
{
"success": false,
"message": "Validation failed",
"status_code": 400,
"data": {
"field_name": ["Error message"]
},
"error_code": "VALIDATION_ERROR"
}
Note: In the examples below, the wrapper format is shown for clarity. All responses follow this pattern.
Table of Contents
- List Users
- Create User
- Get User Details
- Update User
- Delete User (Soft Delete)
- Get Current User
- User Attributes
- API Token Management (Knox)
- Error Codes
- Important Notes
- Complete Workflow Example
- Integration with Social Authentication
1. List Users
Get a list of all users in the current tenant.
Endpoint: GET /api/users/
Authentication: Required
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
search | string | Search by username, email, first_name, or last_name |
is_active | boolean | Filter by active status (true/false) |
is_staff | boolean | Filter by staff status (true/false) |
is_superuser | boolean | Filter by superuser status (true/false) |
is_deleted | boolean | Filter by deleted status (true/false) |
ordering | string | Order by field (e.g., username, -date_joined, email) |
page | integer | Page number for pagination |
page_size | integer | Number of results per page |
Example Request:
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Example Request with Filters:
# Search for users
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/?search=john" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# Get only active users
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/?is_active=true" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# Get staff users ordered by date joined
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/?is_staff=true&ordering=-date_joined" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Response (200 OK):
{
"success": true,
"message": "Data retrieved successfully",
"status_code": 200,
"data": [
{
"id": 1,
"username": "john.doe",
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"is_active": true,
"is_staff": false,
"is_deleted": false,
"date_joined": "2025-01-15T10:30:00Z",
"last_login": "2025-01-20T14:22:00Z",
"attributes": {}
},
{
"id": 2,
"username": "jane.smith",
"email": "jane.smith@example.com",
"first_name": "Jane",
"last_name": "Smith",
"full_name": "Jane Smith",
"is_active": true,
"is_staff": true,
"is_deleted": false,
"date_joined": "2025-01-14T09:15:00Z",
"last_login": "2025-01-19T16:45:00Z",
"attributes": {
"department": "HR",
"phone_number": "1234567890"
}
}
],
"total": 10,
"page": 1,
"page_size": 10,
"total_pages": 1
}
Note:
- Returns all users by default (including soft-deleted users)
- Non-superusers can only see active users
- Use
?is_deleted=falseto filter out deleted users - Default ordering is by most recent (
-date_joined)
2. Create User
Create a new user in the current tenant.
Endpoint: POST /api/users/
Authentication: Required
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
username | string | Yes | Unique username for the user |
email | string | Yes | Unique email address |
password | string | Yes* | User password (min 8 characters recommended) |
confirm_password | string | Yes* | Must match password field |
first_name | string | No | User's first name |
last_name | string | No | User's last name |
is_active | boolean | No | Active status (default: true) |
is_staff | boolean | No | Staff status (default: false) |
*Note: If password is not provided, the user will have an unusable password (suitable for OAuth-only users).
Example 1: Create Standard User
Request:
curl -X POST "http://tenant1.127.0.0.1.nip.io:8000/api/users/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "john.doe",
"email": "john.doe@example.com",
"password": "SecurePass123!",
"confirm_password": "SecurePass123!",
"first_name": "John",
"last_name": "Doe",
"is_active": true
}'
Response (201 Created):
{
"success": true,
"message": "User created successfully",
"status_code": 201,
"data": {
"id": 5,
"username": "john.doe",
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"is_deleted": false,
"date_joined": "2025-01-20T15:30:00Z",
"last_login": null,
"groups": [],
"user_permissions": [],
"attributes": {},
"missing_attributes": {}
}
}
Example 2: Create Staff User
Request:
curl -X POST "http://tenant1.127.0.0.1.nip.io:8000/api/users/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "admin.user",
"email": "admin@example.com",
"password": "AdminPass123!",
"confirm_password": "AdminPass123!",
"first_name": "Admin",
"last_name": "User",
"is_active": true,
"is_staff": true
}'
Example 3: Create OAuth-Only User (No Password)
Request:
curl -X POST "http://tenant1.127.0.0.1.nip.io:8000/api/users/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "oauth.user",
"email": "oauth@example.com",
"first_name": "OAuth",
"last_name": "User"
}'
Note: This user can only authenticate via social login (OAuth).
Validation Errors
Missing Required Field:
{
"success": false,
"message": "User validation failed",
"status_code": 400,
"data": {
"username": ["This field is required."]
},
"error_code": "VALIDATION_ERROR"
}
Passwords Don't Match:
{
"success": false,
"message": "User validation failed",
"status_code": 400,
"data": {
"confirm_password": ["Passwords do not match."]
},
"error_code": "VALIDATION_ERROR"
}
Duplicate Username:
{
"success": false,
"message": "User validation failed",
"status_code": 400,
"data": {
"username": ["A user with this username already exists."]
},
"error_code": "VALIDATION_ERROR"
}
Duplicate Email:
{
"success": false,
"message": "User validation failed",
"status_code": 400,
"data": {
"email": ["A user with this email already exists."]
},
"error_code": "VALIDATION_ERROR"
}
3. Get User Details
Retrieve detailed information about a specific user, including their groups and permissions.
Endpoint: GET /api/users/{username}/
Authentication: Required
Example Request:
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/john.doe/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Response (200 OK):
{
"success": true,
"message": "User retrieved successfully",
"status_code": 200,
"data": {
"id": 5,
"username": "john.doe",
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"is_deleted": false,
"date_joined": "2025-01-20T15:30:00Z",
"last_login": "2025-01-21T10:15:00Z",
"groups": [
{
"id": 1,
"name": "Editors"
},
{
"id": 3,
"name": "Viewers"
}
],
"user_permissions": [
{
"id": 24,
"name": "Can view log entry",
"codename": "view_logentry",
"content_type": "admin.logentry"
},
{
"id": 32,
"name": "Can add user",
"codename": "add_user",
"content_type": "auth.user"
}
],
"attributes": {},
"missing_attributes": {}
}
}
Note: This endpoint includes user's groups and individual permissions, which are NOT included in the list view for performance reasons.
4. Update User
Update an existing user's information. Password updates are NOT allowed through this endpoint.
Endpoint: PUT /api/users/{username}/
Authentication: Required
Request Body: Any fields from Create User except password and confirm_password
Important Notes:
- Password cannot be updated through this endpoint (use password reset flow)
is_deletedfield cannot be updated (use DELETE endpoint for soft delete)- All fields are optional (partial updates supported)
Example 1: Update User Information
Request:
curl -X PUT "http://tenant1.127.0.0.1.nip.io:8000/api/users/john.doe/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Jonathan",
"last_name": "Doe",
"email": "jonathan.doe@example.com"
}'
Example 2: Deactivate User
Request:
curl -X PUT "http://tenant1.127.0.0.1.nip.io:8000/api/users/john.doe/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"is_active": false
}'
Example 3: Promote User to Staff
Request:
curl -X PUT "http://tenant1.127.0.0.1.nip.io:8000/api/users/john.doe/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"is_staff": true
}'
Response (200 OK):
{
"success": true,
"message": "User updated successfully",
"status_code": 200,
"data": {
"id": 5,
"username": "john.doe",
"email": "jonathan.doe@example.com",
"first_name": "Jonathan",
"last_name": "Doe",
"full_name": "Jonathan Doe",
"is_active": true,
"is_staff": true,
"is_superuser": false,
"is_deleted": false,
"date_joined": "2025-01-20T15:30:00Z",
"last_login": "2025-01-21T10:15:00Z",
"groups": [],
"user_permissions": [],
"attributes": {},
"missing_attributes": {}
}
}
Error: Attempting Password Update
Request:
curl -X PUT "http://tenant1.127.0.0.1.nip.io:8000/api/users/john.doe/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"password": "NewPassword123!"
}'
Response (400 Bad Request):
{
"success": false,
"message": "User validation failed",
"status_code": 400,
"data": {
"password": ["Password cannot be updated through this endpoint."]
},
"error_code": "VALIDATION_ERROR"
}
5. Delete User (Soft Delete)
Soft delete a user by setting their is_deleted flag to true. The user record is not removed from the database.
Endpoint: DELETE /api/users/{username}/
Authentication: Required
Restrictions:
- Users cannot delete themselves
- Non-superusers cannot delete superusers
- This is a soft delete (user record remains in database)
Example Request:
curl -X DELETE "http://tenant1.127.0.0.1.nip.io:8000/api/users/john.doe/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Success Response (200 OK):
{
"success": true,
"message": "User deleted successfully.",
"status_code": 200
}
Error Responses
Attempting to Delete Self:
{
"success": false,
"message": "You cannot delete your own account.",
"status_code": 400
}
Non-Superuser Attempting to Delete Superuser:
{
"success": false,
"message": "You do not have permission to delete superusers.",
"status_code": 403
}
User Not Found (404):
{
"detail": "Not found."
}
6. Get Current User (Me)
Get details of the currently authenticated user.
Endpoint: GET /api/users/me/
Authentication: Required
Example Request:
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/me/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Response (200 OK):
{
"success": true,
"message": "User retrieved successfully",
"status_code": 200,
"data": {
"id": 1,
"username": "current.user",
"email": "current@example.com",
"first_name": "Current",
"last_name": "User",
"full_name": "Current User",
"is_active": true,
"is_staff": true,
"is_superuser": false,
"is_deleted": false,
"date_joined": "2025-01-10T08:00:00Z",
"last_login": "2025-01-21T11:30:00Z",
"groups": [
{
"id": 2,
"name": "Administrators"
}
],
"user_permissions": [],
"attributes": {},
"missing_attributes": {}
}
}
Use Case: This endpoint is useful for:
- Displaying current user information in UI
- Checking current user's permissions
- Profile pages
- Navigation/header user info
7. User Attributes
User attributes are tenant-specific custom fields stored in each user's attributes JSONField. Unlike standard Django user fields (username, email, etc.), attributes are:
- Dynamic: Defined per-tenant using JSON Schema
- Flexible: Can have different attributes for different tenants
- Validated: Automatically validated against the tenant's schema
- Optional: Not all users need to have all attributes immediately
7.1. Schema Management API
Get Current Attributes Schema
Retrieve the current tenant's user attributes schema.
Endpoint: GET /api/users/attributes/
Authentication: Required
Request:
curl -X GET http://localhost:8000/api/users/attributes/ \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Response (200 OK):
{
"success": true,
"message": "User attributes schema retrieved successfully",
"status_code": 200,
"data": {
"type": "object",
"title": "User Attributes",
"properties": {
"department": {
"type": ["string", "null"],
"title": "Department",
"enum": ["HR", "DEV", "MANAGER", "SALES", "SUPPORT"]
},
"phone_number": {
"type": ["string", "null"],
"title": "Phone Number",
"minLength": 10,
"maxLength": 15
},
"emp_no": {
"type": ["string", "null"],
"title": "Employee Number",
"pattern": "^EMP[0-9]{5}$"
}
},
"required": ["department", "phone_number"]
}
}
Empty Schema Response: If no schema is configured, returns:
{
"success": true,
"message": "User attributes schema retrieved successfully",
"status_code": 200,
"data": {}
}
Update Attributes Schema
Update or create the user attributes schema for the current tenant.
Endpoint: POST /api/users/attributes/
Authentication: Required (Admin only - is_staff or is_superuser)
Request Body: Direct JSON Schema (not wrapped)
Request:
curl -X POST http://localhost:8000/api/users/attributes/ \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "object",
"title": "Employee Attributes",
"properties": {
"department": {
"type": ["string", "null"],
"title": "Department",
"enum": ["HR", "DEV", "MANAGER", "SALES", "SUPPORT"]
},
"phone_number": {
"type": ["string", "null"],
"title": "Phone Number",
"minLength": 10,
"maxLength": 15
},
"emp_no": {
"type": ["string", "null"],
"title": "Employee Number",
"minLength": 8,
"maxLength": 20
}
},
"required": ["department", "phone_number"]
}'
Response (200 OK):
{
"success": true,
"message": "User attributes schema updated successfully",
"status_code": 200,
"data": {
"type": "object",
"title": "Employee Attributes",
"properties": {
"department": {
"type": ["string", "null"],
"title": "Department",
"enum": ["HR", "DEV", "MANAGER", "SALES", "SUPPORT"]
},
"phone_number": {
"type": ["string", "null"],
"title": "Phone Number",
"minLength": 10,
"maxLength": 15
},
"emp_no": {
"type": ["string", "null"],
"title": "Employee Number",
"minLength": 8,
"maxLength": 20
}
},
"required": ["department", "phone_number"]
}
}
Error Response (403 Forbidden):
{
"success": false,
"message": "Only administrators can update attributes schema",
"status_code": 403
}
The POST endpoint replaces the entire schema. To add a new property, you must send the complete schema including all existing properties.
Workflow: GET current schema → Modify → POST updated schema
7.2. JSON Schema Format
User attributes use JSON Schema Draft 2020-12 format (latest standard).
Required Structure
{
"type": "object", // REQUIRED: Must be "object"
"title": "...", // Optional: Display name
"properties": { // REQUIRED: Define your custom fields
"field_name": {
"type": ["string", "null"], // Type with null support
"title": "...", // Display name
// ... additional constraints
}
},
"required": ["field1", "field2"] // Optional: Required fields
}
Supported Field Types
{
"properties": {
"text_field": {
"type": ["string", "null"],
"minLength": 5,
"maxLength": 100,
"pattern": "^[A-Z]+$"
},
"number_field": {
"type": ["number", "null"],
"minimum": 0,
"maximum": 100
},
"integer_field": {
"type": ["integer", "null"],
"minimum": 1
},
"boolean_field": {
"type": ["boolean", "null"]
},
"enum_field": {
"type": ["string", "null"],
"enum": ["option1", "option2", "option3"]
}
}
}
Always include "null" in the type array for optional flexibility:
"type": ["string", "null"] // ✅ Recommended
"type": "string" // ❌ Too strict
This allows:
- Partial updates: Update only some attributes without providing all
- Removing attributes: Set a value to
nullto clear it - Gradual completion: Users can fill required fields over time
7.3. Working with User Attributes
Creating a User with Attributes
Endpoint: POST /api/users/
Request:
curl -X POST http://localhost:8000/api/users/ \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"email": "john@example.com",
"password": "SecurePass123!",
"confirm_password": "SecurePass123!",
"first_name": "John",
"last_name": "Doe",
"attributes": {
"department": "DEV",
"phone_number": "1234567890",
"emp_no": "EMP12345"
}
}'
Response (201 Created):
{
"id": 42,
"username": "john_doe",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"attributes": {
"department": "DEV",
"phone_number": "1234567890",
"emp_no": "EMP12345"
},
"missing_attributes": {},
"groups": [],
"user_permissions": []
}
Updating User Attributes (Partial Update)
Endpoint: PUT /api/users/{username}/
The update endpoint supports partial updates - you only need to send the attributes you want to change.
Request (Update single attribute):
curl -X PUT http://localhost:8000/api/users/john_doe/ \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"department": "MANAGER"
}
}'
Before:
{
"attributes": {
"department": "DEV",
"phone_number": "1234567890",
"emp_no": "EMP12345"
}
}
After:
{
"attributes": {
"department": "MANAGER", // ← Updated
"phone_number": "1234567890", // ← Preserved
"emp_no": "EMP12345" // ← Preserved
}
}
Attributes are merged, not replaced. Existing attributes are preserved unless explicitly updated or removed.
Removing Attributes
To remove an optional attribute from a user, send null as the value.
Request:
curl -X PUT http://localhost:8000/api/users/john_doe/ \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"emp_no": null
}
}'
Before:
{
"attributes": {
"department": "HR",
"phone_number": "1234567890",
"emp_no": "EMP12345"
}
}
After:
{
"attributes": {
"department": "HR",
"phone_number": "1234567890",
"emp_no": null
}
}
You cannot set a required attribute to null. The validation will fail:
// Request
{"attributes": {"department": null}}
// Response (400 Bad Request)
{
"attributes.department": [
"None is not of type 'string'"
]
}
7.4. Missing Attributes Detection
When a user is missing required attributes, the API automatically detects and returns them in the missing_attributes field.
How It Works
- Schema defines:
"required": ["department", "phone_number"] - User has:
{"department": "HR"} - API returns:
{"missing_attributes": {"phone_number": {...}}}
Response Format
The missing_attributes field returns a dictionary with missing field names as keys and their JSON Schema definitions as values:
{
"username": "john_doe",
"attributes": {
"department": "HR"
},
"missing_attributes": {
"phone_number": {
"type": ["string", "null"],
"title": "Phone Number",
"minLength": 10,
"maxLength": 15
}
}
}
Using Missing Attributes in Frontend
// Fetch user details
const response = await fetch('/api/users/john_doe/', {
headers: { 'Authorization': `Bearer ${token}` }
});
const user = await response.json();
// Check if user has missing required attributes
if (Object.keys(user.missing_attributes).length > 0) {
// Show form to complete profile
showProfileCompletionForm(user.missing_attributes);
}
The missing_attributes field only appears in detail view (GET /api/users/{username}/), not in list view, for performance reasons.
7.5. Validation Rules
User attributes are validated at the model level (in the User model's save() method), ensuring validation occurs regardless of how the user is created - via API, Django admin, management commands, or any other method.
Reserved Field Names
The following field names are reserved and cannot be used as custom attribute names:
- Identity:
id,pk,uuid,username,email,password - Profile:
first_name,last_name,full_name - Status:
is_active,is_staff,is_superuser,is_deleted - Timestamps:
date_joined,last_login,created_at,updated_at - Relations:
groups,user_permissions - Custom:
attributes
Error Example:
{
"error": "Attribute name 'email' is reserved and cannot be used (conflicts with User model field)"
}
Field Name Format
Attribute names must:
- Start with a lowercase letter
- Contain only lowercase letters, numbers, and underscores
- Match regex:
^[a-z][a-z0-9_]*$
Valid: department, phone_number, emp_no, user_level_2
Invalid: Department, phone-number, 2nd_phone, _private
7.6. Example Use Cases
Example 1: Employee Management
{
"type": "object",
"title": "Employee Attributes",
"properties": {
"employee_id": {
"type": ["string", "null"],
"title": "Employee ID",
"pattern": "^EMP[0-9]{5}$"
},
"department": {
"type": ["string", "null"],
"title": "Department",
"enum": ["HR", "Engineering", "Sales", "Marketing", "Finance"]
},
"job_title": {
"type": ["string", "null"],
"title": "Job Title"
},
"manager_email": {
"type": ["string", "null"],
"title": "Manager Email",
"format": "email"
},
"hire_date": {
"type": ["string", "null"],
"title": "Hire Date",
"format": "date"
}
},
"required": ["employee_id", "department", "hire_date"]
}
Example 2: Customer Portal
{
"type": "object",
"title": "Customer Attributes",
"properties": {
"company_name": {
"type": ["string", "null"],
"title": "Company Name",
"minLength": 2,
"maxLength": 100
},
"industry": {
"type": ["string", "null"],
"title": "Industry",
"enum": ["Technology", "Healthcare", "Finance", "Retail", "Other"]
},
"customer_tier": {
"type": ["string", "null"],
"title": "Customer Tier",
"enum": ["Free", "Pro", "Enterprise"]
},
"annual_revenue": {
"type": ["number", "null"],
"title": "Annual Revenue (USD)",
"minimum": 0
}
},
"required": ["company_name", "customer_tier"]
}
7.7. Best Practices
1. Always Include null in Type Arrays
// ✅ Good: Allows flexibility
"type": ["string", "null"]
// ❌ Bad: Too restrictive
"type": "string"
2. Mark Only Essential Fields as Required
Only mark fields as required if they are absolutely necessary for your application to function. This allows users to gradually complete their profiles.
// ✅ Good: Only critical fields required
"required": ["department"]
// ❌ Bad: Too many required fields
"required": ["department", "phone", "address", "title", "manager"]
3. Use Descriptive Titles
Provide clear title fields for all properties to help frontend developers build better UIs:
{
"emp_no": {
"type": ["string", "null"],
"title": "Employee Number", // ✅ Clear
"description": "Format: EMP12345" // ✅ Helpful
}
}
4. Schema Update Workflow
When updating schemas:
- GET current schema first
- Extract the schema from the data field
- Modify locally
- POST complete updated schema
// Fetch current schema
const response = await fetch('/api/users/attributes/');
const schemaResponse = await response.json();
const currentSchema = schemaResponse.data;
// Add new field
currentSchema.properties.new_field = {
type: ["string", "null"],
title: "New Field"
};
// Update schema
await fetch('/api/users/attributes/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentSchema)
});
5. Validate Client-Side Before Sending
Use the schema to validate on the frontend before making API calls:
import Ajv from 'ajv';
const ajv = new Ajv();
const schema = await fetch('/api/users/attributes/').then(r => r.json());
const validate = ajv.compile(schema);
const valid = validate(userAttributes);
if (!valid) {
console.log(validate.errors);
showValidationErrors(validate.errors);
return;
}
// Proceed with API call
await updateUser(username, { attributes: userAttributes });
8. API Token Management (Knox)
The API Token system provides Personal Access Tokens (PATs) for programmatic API access. Tokens act as an alternative to JWT authentication and inherit all permissions from the associated user account.
Base URL: /api/users/token/
Authentication for Token Management:
- Creating tokens: JWT, Session, or XSessionToken (Knox tokens cannot create new tokens)
- Using tokens: Any endpoint accepts
Authorization: Api-Key <token>
Key Features:
- Secure: Tokens hashed with SHA-512, full token shown only once
- Flexible Expiry: Set custom expiration or create infinite tokens
- Named Tokens: Assign human-readable names for easy identification
- Permission Inheritance: Tokens have all permissions of the user
- Unlimited: No limit on tokens per user
API tokens are equivalent to passwords. Store them securely and never commit them to version control. If a token is compromised, revoke it immediately using the DELETE endpoint.
8.1. Create API Token
Create a new authentication token for the authenticated user.
Endpoint: POST /api/users/token/
Authentication: JWT, Session, or XSessionToken (NOT Knox tokens)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable identifier (max 50 chars) |
expiry | datetime/null | Yes | ISO 8601 datetime or null for infinite token |
Set expiry to null to create tokens that never expire. Use this for long-term integrations, CI/CD pipelines, or service accounts.
Example 1: Create Token with Expiry
Request:
curl -X POST "http://localhost:8000/api/users/token/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "30-Day Production Token",
"expiry": "2025-12-31T23:59:59Z"
}'
Response (201 Created):
{
"success": true,
"message": "Token created successfully. Please save this token securely as it cannot be retrieved again.",
"status_code": 201,
"data": {
"id": "7b887f2044dde644107cbbe9225f0ce7ba749d4605d76f99672982128484d5d1...",
"name": "30-Day Production Token",
"token": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"created": "2025-01-19T10:30:00Z",
"expiry": "2025-12-31T23:59:59Z"
}
}
Example 2: Create Infinite Token
Request:
curl -X POST "http://localhost:8000/api/users/token/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "CI/CD Pipeline Token",
"expiry": null
}'
Response (201 Created):
{
"success": true,
"message": "Token created successfully. Please save this token securely as it cannot be retrieved again.",
"status_code": 201,
"data": {
"id": "ad968e2c99ce6b12e10d5aa3c4d115eedc6db2652a80a0efe7068fb13f14c41e...",
"name": "CI/CD Pipeline Token",
"token": "5935b23a252169a48d3fff3524aaee8f30a1e46630b94afe794da74cf6beb615",
"created": "2025-01-19T10:35:00Z",
"expiry": null
}
}
The token field (inside data) contains the full plaintext token and is shown ONLY ONCE. Save it immediately in a secure location (password manager, secrets vault, environment variable). It cannot be retrieved again.
The id field is the token's digest (hash) and can be used to identify and revoke the token later.
Validation Errors
Missing Name:
{
"success": false,
"message": "Validation failed",
"status_code": 400,
"data": {
"name": ["This field is required."]
},
"error_code": "VALIDATION_ERROR"
}
Missing Expiry:
{
"success": false,
"message": "Validation failed",
"status_code": 400,
"data": {
"expiry": ["This field is required."]
},
"error_code": "VALIDATION_ERROR"
}
Past Expiry Date:
{
"success": false,
"message": "Validation failed",
"status_code": 400,
"data": {
"expiry": ["Expiry date must be in the future"]
},
"error_code": "VALIDATION_ERROR"
}
Invalid Expiry Format:
{
"success": false,
"message": "Validation failed",
"status_code": 400,
"data": {
"expiry": ["Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]."]
},
"error_code": "VALIDATION_ERROR"
}
8.2. List API Tokens
Retrieve all API tokens for the authenticated user. Returns metadata only (not the full tokens).
Endpoint: GET /api/users/token/
Authentication: Any authentication method (including Knox tokens)
Example Request:
curl -X GET "http://localhost:8000/api/users/token/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Response (200 OK):
{
"success": true,
"message": "Tokens retrieved successfully",
"status_code": 200,
"data": [
{
"id": "7b887f2044dde644107cbbe9225f0ce7ba749d4605d76f99672982128484d5d1...",
"name": "30-Day Production Token",
"created": "2025-01-19T10:30:00Z",
"expiry": "2025-12-31T23:59:59Z"
},
{
"id": "ad968e2c99ce6b12e10d5aa3c4d115eedc6db2652a80a0efe7068fb13f14c41e...",
"name": "CI/CD Pipeline Token",
"created": "2025-01-19T10:35:00Z",
"expiry": null
},
{
"id": "5e91e13f250e058977dbd8724a95795bd776cfe88dd25604a57b5eac8ed33ae6...",
"name": "Development Token",
"created": "2025-01-18T14:20:00Z",
"expiry": "2025-02-18T14:20:00Z"
}
],
"total": 3
}
Empty Response (No Tokens):
{
"success": true,
"message": "Tokens retrieved successfully",
"status_code": 200,
"data": [],
"total": 0
}
Notes:
- Tokens ordered by creation date (newest first)
- Only returns tokens for authenticated user
- Full token value is NEVER shown after creation
- Use for auditing, token management UI, identifying which tokens to revoke
8.3. Revoke API Token
Permanently delete an API token. This immediately invalidates the token.
Endpoint: DELETE /api/users/token/{id}/
Authentication: JWT, Session, or XSessionToken recommended
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Token digest (from create/list response) |
Example Request:
curl -X DELETE "http://localhost:8000/api/users/token/7b887f2044dde644107cbbe9225f0ce7ba749d4605d76f99672982128484d5d1fcab8d5ecbe530f75dfafd415df5dff97b34e84b5086acb5303bb3f6cd100943/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Success Response (204 No Content):
{
"success": true,
"message": "Token revoked successfully",
"status_code": 204
}
Error Response (404 Not Found):
{
"success": false,
"message": "Token not found",
"status_code": 404
}
Notes:
- Token is permanently deleted from database
- Cannot revoke tokens belonging to other users (404)
- Revoked tokens immediately stop working
- Cannot undo revocation - must create new token if needed
8.4. Using API Tokens for Authentication
Once created, use API tokens in the Authorization header to authenticate requests to any API endpoint.
Header Format
Authorization: Api-Key <your_token_here>
The header prefix is Api-Key (NOT Bearer, Token, or Api-Token). Using the wrong prefix will result in 401 Unauthorized errors.
Example Usage
Get User List:
curl -X GET "http://localhost:8000/api/users/" \
-H "Authorization: Api-Key 5935b23a252169a48d3fff3524aaee8f30a1e46630b94afe794da74cf6beb615"
Create New User:
curl -X POST "http://localhost:8000/api/users/" \
-H "Authorization: Api-Key 5935b23a252169a48d3fff3524aaee8f30a1e46630b94afe794da74cf6beb615" \
-H "Content-Type: application/json" \
-d '{
"username": "newuser",
"email": "newuser@example.com",
"password": "SecurePass123!",
"confirm_password": "SecurePass123!"
}'
Get Current User:
curl -X GET "http://localhost:8000/api/users/me/" \
-H "Authorization: Api-Key 5935b23a252169a48d3fff3524aaee8f30a1e46630b94afe794da74cf6beb615"
Permission Inheritance
API tokens inherit ALL permissions and roles from the associated user account:
- ✅ Superuser token: Has all permissions across all resources
- ✅ Staff token: Can access admin-restricted endpoints
- ✅ Regular user token: Limited to user's assigned permissions
- ✅ Group permissions: Token inherits user's group memberships
- ✅ Object permissions: Works with Django Guardian object-level permissions
- ✅ Tenant context: Automatically scoped to user's tenant
Example - Superuser Token:
# Token from superuser account can delete users
curl -X DELETE "http://localhost:8000/api/users/some-user/" \
-H "Authorization: Api-Key <superuser_token>"
# ✅ Success
Example - Regular User Token:
# Token from regular user cannot delete users
curl -X DELETE "http://localhost:8000/api/users/some-user/" \
-H "Authorization: Api-Key <regular_user_token>"
# ❌ 403 Forbidden
8.5. Token Expiry Behavior
Tokens support two expiry modes: time-based and infinite.
Time-Based Tokens
Tokens with a specific expiry datetime become invalid after that time.
Creation:
{
"name": "Temporary API Key",
"expiry": "2025-06-30T23:59:59Z"
}
Behavior:
- ✅ Works before
2025-06-30T23:59:59Z - ❌ Returns 401 Unauthorized after expiry
- 🔒 No auto-refresh - expiry never extends
- 📅 Check expiry via list endpoint
Use Cases:
- Temporary contractor access
- Time-limited integrations
- Demo/trial API keys
- Compliance requirements (rotate every 90 days)
Infinite Tokens
Tokens with expiry: null never expire automatically.
Creation:
{
"name": "Permanent Service Account",
"expiry": null
}
Behavior:
- ✅ Works indefinitely until manually revoked
- 🔒 Must be explicitly deleted to invalidate
- ⚠️ Higher security risk if compromised
Use Cases:
- CI/CD pipelines
- Long-running services
- Server-to-server communication
- Internal automation tools
Even for infinite tokens, implement regular rotation (e.g., every 6-12 months) by creating a new token and revoking the old one. Many organizations enforce token rotation policies for compliance.
8.6. Security Considerations
Server-Side Security
Token Hashing:
- Tokens hashed with SHA-512 before storage
- Only digest (hash) stored in database
- Full token cannot be recovered from database
- First 8 characters stored as
token_keyfor identification
Storage:
Database Storage:
- digest: 7b887f204... (SHA-512 hash - PRIMARY KEY)
- token_key: 5935b23a (First 8 chars for ID)
- name: "My Token"
- expiry: "2025-12-31T23:59:59Z"
- user_id: 1
Authentication Flow:
- Client sends:
Authorization: Api-Key 5935b23a252169a4... - Server hashes received token with SHA-512
- Server looks up hash in database
- If match found and not expired → Authenticated
- Sets
request.userto token owner
Client-Side Security
DO:
- ✅ Store tokens in environment variables
- ✅ Use secrets managers (AWS Secrets Manager, HashiCorp Vault, etc.)
- ✅ Store in password managers for personal use
- ✅ Limit token scope to minimum required permissions
- ✅ Create separate tokens for different services/environments
- ✅ Rotate tokens regularly
- ✅ Revoke tokens immediately if compromised
- ✅ Monitor token usage logs
DON'T:
- ❌ Commit tokens to version control (.env.example OK, .env NOT OK)
- ❌ Share tokens via email/Slack/chat
- ❌ Store tokens in client-side JavaScript
- ❌ Log full tokens in application logs
- ❌ Use same token across multiple environments
- ❌ Leave unused tokens active
Example - Environment Variables:
# .env (NEVER commit this file)
API_TOKEN=5935b23a252169a48d3fff3524aaee8f30a1e46630b94afe794da74cf6beb615
# .env.example (safe to commit)
API_TOKEN=your_token_here
Example - Docker Secrets:
# docker-compose.yml
services:
app:
environment:
- API_TOKEN_FILE=/run/secrets/api_token
secrets:
- api_token
secrets:
api_token:
external: true
Example - Python Usage:
import os
import requests
# ✅ Load from environment
API_TOKEN = os.environ['API_TOKEN']
# ❌ NEVER hardcode
# API_TOKEN = "5935b23a252169a4..." # DON'T DO THIS!
headers = {'Authorization': f'Api-Key {API_TOKEN}'}
response = requests.get('http://localhost:8000/api/users/', headers=headers)
8.7. Complete Workflow Example
This example demonstrates the full lifecycle of API token management:
# 1. Authenticate with JWT to get access token
JWT_TOKEN=$(curl -X POST "http://localhost:8000/api/auth/jwt/token/" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}' \
| jq -r '.access')
# 2. Create an infinite API token for CI/CD
TOKEN_RESPONSE=$(curl -X POST "http://localhost:8000/api/users/token/" \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "GitHub Actions CI",
"expiry": null
}')
# 3. Extract and save the token (ONLY SHOWN ONCE!)
API_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.data.token')
TOKEN_ID=$(echo $TOKEN_RESPONSE | jq -r '.data.id')
echo "Save this token securely: $API_TOKEN"
echo "Token ID for revocation: $TOKEN_ID"
# 4. Test the token by fetching users
curl -X GET "http://localhost:8000/api/users/" \
-H "Authorization: Api-Key $API_TOKEN"
# 5. Create another token with 90-day expiry
TEMP_TOKEN_RESPONSE=$(curl -X POST "http://localhost:8000/api/users/token/" \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Contractor Access\",
\"expiry\": \"$(date -d '+90 days' -u +%Y-%m-%dT%H:%M:%SZ)\"
}")
TEMP_TOKEN=$(echo $TEMP_TOKEN_RESPONSE | jq -r '.data.token')
TEMP_TOKEN_ID=$(echo $TEMP_TOKEN_RESPONSE | jq -r '.data.id')
# 6. List all active tokens
curl -X GET "http://localhost:8000/api/users/token/" \
-H "Authorization: Bearer $JWT_TOKEN"
# 7. Use token in automated script
curl -X POST "http://localhost:8000/api/users/" \
-H "Authorization: Api-Key $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "automated.user",
"email": "auto@example.com",
"password": "SecurePass123!",
"confirm_password": "SecurePass123!"
}'
# 8. Revoke the temporary token when contractor leaves
curl -X DELETE "http://localhost:8000/api/users/token/$TEMP_TOKEN_ID/" \
-H "Authorization: Bearer $JWT_TOKEN"
# 9. Verify token was revoked (should return 401)
curl -X GET "http://localhost:8000/api/users/" \
-H "Authorization: Api-Key $TEMP_TOKEN"
8.8. Integration Examples
CI/CD Pipeline (GitHub Actions)
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Deploy via API
env:
API_TOKEN: ${{ secrets.PRODUCTION_API_TOKEN }}
run: |
curl -X POST "https://api.example.com/api/deployments/" \
-H "Authorization: Api-Key $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"version": "${{ github.sha }}"}'
Python Script
import os
import requests
class APIClient:
def __init__(self):
self.base_url = os.environ['API_BASE_URL']
self.token = os.environ['API_TOKEN']
self.headers = {
'Authorization': f'Api-Key {self.token}',
'Content-Type': 'application/json'
}
def get_users(self):
response = requests.get(
f'{self.base_url}/api/users/',
headers=self.headers
)
response.raise_for_status()
return response.json()
def create_user(self, user_data):
response = requests.post(
f'{self.base_url}/api/users/',
headers=self.headers,
json=user_data
)
response.raise_for_status()
return response.json()
# Usage
client = APIClient()
users_response = client.get_users()
print(f"Found {users_response['total']} users")
Node.js / JavaScript
// api-client.js
const axios = require('axios');
class APIClient {
constructor() {
this.baseURL = process.env.API_BASE_URL;
this.token = process.env.API_TOKEN;
this.client = axios.create({
baseURL: this.baseURL,
headers: {
'Authorization': `Api-Key ${this.token}`,
'Content-Type': 'application/json'
}
});
}
async getUsers() {
const response = await this.client.get('/api/users/');
return response.data;
}
async createUser(userData) {
const response = await this.client.post('/api/users/', userData);
return response.data;
}
}
// Usage
const client = new APIClient();
client.getUsers()
.then(data => console.log(`Found ${data.total} users`))
.catch(error => console.error('Error:', error.message));
cURL Automation Script
#!/bin/bash
# bulk-user-import.sh
set -e # Exit on error
API_BASE_URL="https://api.example.com"
API_TOKEN="${API_TOKEN:-}" # From environment
if [ -z "$API_TOKEN" ]; then
echo "Error: API_TOKEN environment variable not set"
exit 1
fi
# Function to create user
create_user() {
local username=$1
local email=$2
curl -X POST "$API_BASE_URL/api/users/" \
-H "Authorization: Api-Key $API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"username\": \"$username\",
\"email\": \"$email\",
\"password\": \"TempPass123!\",
\"confirm_password\": \"TempPass123!\"
}" \
-w "\nHTTP Status: %{http_code}\n"
}
# Bulk create from CSV
while IFS=, read -r username email; do
echo "Creating user: $username"
create_user "$username" "$email"
sleep 0.5 # Rate limiting
done < users.csv
8.9. Troubleshooting
Common Issues
Problem: 401 "Authentication credentials were not provided"
Solution: Check header format - must be Authorization: Api-Key <token>, not Bearer or Token
# ❌ Wrong
Authorization: Bearer 5935b23a252169a4...
Authorization: Token 5935b23a252169a4...
# ✅ Correct
Authorization: Api-Key 5935b23a252169a4...
Problem: 401 "Invalid token"
Causes:
- Token expired (check expiry date)
- Token was revoked
- Token string corrupted (missing characters, extra spaces)
- Wrong token for this environment
Solution: Create new token or check token value is complete
Problem: Cannot create token with Knox authentication
Expected behavior: Knox tokens cannot create new tokens (prevents infinite token generation)
Solution: Use JWT, Session, or XSessionToken authentication to create tokens
Problem: Lost token value
Unfortunately, tokens cannot be retrieved after creation.
Solution: Revoke old token and create new one:
# 1. List tokens to find ID
curl -X GET "http://localhost:8000/api/users/token/" \
-H "Authorization: Bearer $JWT_TOKEN"
# 2. Revoke lost token
curl -X DELETE "http://localhost:8000/api/users/token/$TOKEN_ID/" \
-H "Authorization: Bearer $JWT_TOKEN"
# 3. Create replacement token
curl -X POST "http://localhost:8000/api/users/token/" \
-H "Authorization: Bearer $JWT_TOKEN" \
-d '{"name":"Replacement Token","expiry":null}'
9. Error Codes
| HTTP Code | Description |
|---|---|
| 200 | Success |
| 201 | User created successfully |
| 400 | Bad request - validation error or operation not allowed |
| 401 | Unauthorized - missing or invalid authentication |
| 403 | Forbidden - insufficient permissions |
| 404 | Not found - user doesn't exist |
| 500 | Internal server error |
Common Error Responses
Authentication Missing:
{
"detail": "Authentication credentials were not provided."
}
Invalid Token:
{
"detail": "Given token not valid for any token type",
"code": "token_not_valid",
"messages": [
{
"token_class": "AccessToken",
"token_type": "access",
"message": "Token is invalid or expired"
}
]
}
User Not Found:
{
"detail": "Not found."
}
9. Important Notes
Soft Delete vs Hard Delete
- This API implements soft delete only
- Deleted users have
is_deleted=truebut remain in the database - Deleted users are still visible in list view (use
?is_deleted=falseto filter) - Non-superusers cannot see deleted users
Password Management
- Creation: Password is required on user creation (unless creating OAuth-only user)
- Updates: Password CANNOT be updated through the update endpoint
- Password Reset: Use Django's built-in password reset flow or implement custom endpoint
- Hashing: Passwords are automatically hashed using Django's password hashers
Multi-Tenancy
- All operations are automatically scoped to the current tenant
- Users are isolated per tenant schema
- Tenant is determined by the domain in the request URL
Permissions and Groups
- Groups and permissions are only shown in the detail view (
GET /api/users/{slug}/) - NOT included in list view for performance optimization
- Use prefetch_related for optimal query performance
Field Restrictions
id: Read-only, auto-generateddate_joined: Read-only, set on creationlast_login: Read-only, updated by Django authis_deleted: Can only be set via DELETE endpointis_superuser: Not exposed in create/update (set via Django admin)
Email and Username Uniqueness
- Both
emailandusernamemust be unique within the tenant - Validation errors returned if duplicates detected
- Case-sensitive comparison
Non-Superuser Restrictions
- Can only view active users
- Cannot see deleted users
- Cannot delete superusers
- Cannot delete themselves
10. Complete Workflow Example
Here's a complete example of user management workflow:
# 1. Authenticate and get JWT token
TOKEN=$(curl -X POST "http://tenant1.127.0.0.1.nip.io:8000/api/auth/jwt/token/" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}' | jq -r '.access')
# 2. Get current user info
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/me/" \
-H "Authorization: Bearer $TOKEN"
# 3. List all active users
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/?is_active=true&is_deleted=false" \
-H "Authorization: Bearer $TOKEN"
# 4. Create a new user
USERNAME=$(curl -X POST "http://tenant1.127.0.0.1.nip.io:8000/api/users/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "new.user",
"email": "new.user@example.com",
"password": "SecurePass123!",
"confirm_password": "SecurePass123!",
"first_name": "New",
"last_name": "User",
"is_active": true
}' | jq -r '.data.username')
# 5. Get new user details
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/$USERNAME/" \
-H "Authorization: Bearer $TOKEN"
# 6. Update user information
curl -X PUT "http://tenant1.127.0.0.1.nip.io:8000/api/users/$USERNAME/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Updated",
"is_staff": true
}'
# 7. Search for users
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/?search=new" \
-H "Authorization: Bearer $TOKEN"
# 8. Soft delete user
curl -X DELETE "http://tenant1.127.0.0.1.nip.io:8000/api/users/$USERNAME/" \
-H "Authorization: Bearer $TOKEN"
# 9. Verify deletion (should show is_deleted=true)
curl -X GET "http://tenant1.127.0.0.1.nip.io:8000/api/users/?is_deleted=true" \
-H "Authorization: Bearer $TOKEN"
11. Integration with Social Authentication
When users log in via OAuth (Google, GitHub, Keycloak, etc.) with SOCIALACCOUNT_AUTO_CONNECT = True:
Auto-Connect Behavior
✅ If a user with the same email exists:
- The OAuth account is linked to the existing user
- No duplicate user is created
- User logs in with their existing account
✅ If no user with that email exists:
- A new user is created automatically
- Username is generated from OAuth provider data
- User has no password (OAuth-only authentication)
OAuth User Characteristics
password: Unusable (empty hash)is_active: True (by default)is_staff: Falseemail: From OAuth providerfirst_name,last_name: From OAuth provider (if available)
You can manage OAuth users through this API like any other user (except password changes).
Support
For issues or questions:
- Check logs:
docker logs taruvi_web - Review admin interface:
/admin/auth/user/ - API documentation:
/api/docs/(Swagger UI) - ReDoc:
/api/redoc/
Last Updated: January 2025 API Version: 1.0.0 Django Version: 5.2.6 DRF Version: 3.15+