Version: 1.0
Last Updated: October 5, 2025
This document provides a comprehensive reference for the Paintjob API. The API allows clients to manage users, projects, rooms, walls, and generate motion paths for painting automation.
http://localhost:8000/api
Most endpoints require JWT Bearer token authentication. Include the token in the Authorization header:
Authorization: Bearer <your_jwt_token>
Tokens are obtained through the /users/login endpoint and expire after a configured time period (check the expires_in field in login responses).
Represents a registered user in the system.
{
"id": 1,
"email": "user@example.com",
"full_name": "John Doe",
"is_admin": false,
"created_at": "2025-10-05T12:00:00"
}Fields:
id(integer): Unique user identifieremail(string): User's email addressfull_name(string, optional): User's full nameis_admin(boolean): Whether the user has admin privilegescreated_at(datetime): Account creation timestamp
Represents a project that contains multiple rooms.
{
"id": 1,
"name": "Home Renovation",
"description": "Main house painting project",
"user_id": 1,
"created_at": "2025-10-05T12:00:00",
"updated_at": "2025-10-05T14:30:00"
}Fields:
id(integer): Unique project identifiername(string, required): Project namedescription(string, optional): Project descriptionuser_id(integer): ID of the user who owns this projectcreated_at(datetime): Project creation timestampupdated_at(datetime, optional): Last update timestamp
Represents a room within a project.
{
"id": 1,
"name": "Living Room",
"project_id": 1,
"data": {
"width": 500,
"height": 300,
"customField": "value"
},
"walls": ["wall-1", "wall-2"],
"created_at": "2025-10-05T12:00:00",
"updated_at": "2025-10-05T14:30:00"
}Fields:
id(integer): Unique room identifiername(string, required): Room nameproject_id(integer, required): ID of the parent projectdata(object, required): JSON object containing room-specific datawalls(array of strings, optional): Array of wall IDs/referencescreated_at(datetime): Room creation timestampupdated_at(datetime, optional): Last update timestamp
Represents a wall within a room with geometry and motion path data.
{
"id": 1,
"wall_id": "wall-front",
"room_id": 1,
"data": {
"version": 1,
"unit": "px",
"origin": "bottom-left",
"wall": {
"width": 620,
"height": 460,
"thickness": 15
},
"obstacles": []
},
"motion_data": {
"strokes": [],
"geojson": {},
"params": {}
},
"created_at": "2025-10-05T12:00:00",
"updated_at": "2025-10-05T14:30:00"
}Fields:
id(integer): Unique database identifierwall_id(string, required): Public wall identifier (e.g., "wall-front")room_id(integer, required): ID of the parent roomdata(object, required): JSON object containing wall geometry datamotion_data(object, optional): Generated motion path payload with strokes and parameterscreated_at(datetime): Wall creation timestampupdated_at(datetime, optional): Last update timestamp
Creates a new user account.
Endpoint: POST /users/
Authentication: None
Request Body:
{
"email": "user@example.com",
"password": "password123",
"full_name": "John Doe"
}Response: 201 Created
{
"id": 1,
"email": "user@example.com",
"full_name": "John Doe",
"is_admin": false,
"created_at": "2025-10-05T12:00:00"
}Error Responses:
409 Conflict: Email already registered
Authenticates a user and returns a JWT token.
Endpoint: POST /users/login
Authentication: None
Rate Limit: 5 attempts per 5 minutes
Request Body:
{
"email": "user@example.com",
"password": "password123"
}Response: 200 OK
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 43200,
"user": {
"id": 1,
"email": "user@example.com",
"full_name": "John Doe",
"is_admin": false,
"created_at": "2025-10-05T12:00:00"
}
}Error Responses:
401 Unauthorized: Invalid credentials429 Too Many Requests: Rate limit exceeded
Returns the authenticated user's profile.
Endpoint: GET /users/me
Authentication: Required
Response: 200 OK
{
"id": 1,
"email": "user@example.com",
"full_name": "John Doe",
"is_admin": false,
"created_at": "2025-10-05T12:00:00"
}Error Responses:
401 Unauthorized: Invalid or missing token
Returns the authenticated user's profile along with all projects and their rooms.
Endpoint: GET /users/me/with-projects
Authentication: Required
Response: 200 OK
{
"id": 1,
"email": "user@example.com",
"full_name": "John Doe",
"is_admin": false,
"created_at": "2025-10-05T12:00:00",
"projects": [
{
"id": 1,
"name": "Home Renovation",
"description": "Main house painting project",
"user_id": 1,
"created_at": "2025-10-05T12:00:00",
"updated_at": "2025-10-05T14:30:00",
"rooms": [
{
"id": 1,
"name": "Living Room",
"project_id": 1,
"data": {},
"walls": [],
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}
]
}
]
}Generates a new JWT token for the authenticated user.
Endpoint: POST /users/refresh
Authentication: Required
Response: 200 OK
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 43200,
"user": {
"id": 1,
"email": "user@example.com",
"full_name": "John Doe",
"is_admin": false,
"created_at": "2025-10-05T12:00:00"
}
}Checks if the current JWT token is valid.
Endpoint: GET /users/validate-token
Authentication: Required
Response: 200 OK
{
"valid": true,
"user": {
"id": 1,
"email": "user@example.com",
"full_name": "John Doe",
"is_admin": false
}
}Client-side token invalidation endpoint.
Endpoint: POST /users/logout
Authentication: None
Response: 200 OK
{
"message": "Successfully logged out. Please remove the token from your client.",
"logout_time": 1696512000
}Creates a new project for the authenticated user.
Endpoint: POST /projects/
Authentication: Required
Request Body:
{
"name": "Home Renovation",
"description": "Main house painting project"
}Response: 201 Created
{
"id": 1,
"name": "Home Renovation",
"description": "Main house painting project",
"user_id": 1,
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}Error Responses:
401 Unauthorized: Not authenticated409 Conflict: Project name already exists for this user
Returns all projects belonging to the authenticated user.
Endpoint: GET /projects/
Authentication: Required
Response: 200 OK
[
{
"id": 1,
"name": "Home Renovation",
"description": "Main house painting project",
"user_id": 1,
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}
]Returns a specific project by ID (must belong to authenticated user).
Endpoint: GET /projects/{project_id}
Authentication: Required
Response: 200 OK
{
"id": 1,
"name": "Home Renovation",
"description": "Main house painting project",
"user_id": 1,
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}Error Responses:
403 Forbidden: Project belongs to another user404 Not Found: Project not found
Returns a project with all its associated rooms.
Endpoint: GET /projects/{project_id}/with-rooms
Authentication: Required
Response: 200 OK
{
"id": 1,
"name": "Home Renovation",
"description": "Main house painting project",
"user_id": 1,
"created_at": "2025-10-05T12:00:00",
"updated_at": null,
"rooms": [
{
"id": 1,
"name": "Living Room",
"data": {},
"walls": [],
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}
]
}Error Responses:
403 Forbidden: Project belongs to another user404 Not Found: Project not found
Updates a project's name or description.
Endpoint: PUT /projects/{project_id}
Authentication: Required
Request Body:
{
"name": "Updated Project Name",
"description": "Updated description"
}Response: 200 OK
{
"id": 1,
"name": "Updated Project Name",
"description": "Updated description",
"user_id": 1,
"created_at": "2025-10-05T12:00:00",
"updated_at": "2025-10-05T15:00:00"
}Error Responses:
403 Forbidden: Project belongs to another user404 Not Found: Project not found409 Conflict: Project name already exists
Deletes a project and all associated rooms and walls.
Endpoint: DELETE /projects/{project_id}
Authentication: Required
Response: 204 No Content
Error Responses:
403 Forbidden: Project belongs to another user404 Not Found: Project not found
Creates a new room within a project.
Endpoint: POST /rooms/
Authentication: None (consider adding if needed)
Request Body:
{
"name": "Living Room",
"project_id": 1,
"data": {
"width": 500,
"height": 300
},
"walls": ["wall-1", "wall-2"]
}Response: 201 Created
{
"id": 1,
"name": "Living Room",
"project_id": 1,
"data": {
"width": 500,
"height": 300
},
"walls": ["wall-1", "wall-2"],
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}Error Responses:
409 Conflict: Room name already exists in this project
Returns all rooms in the system.
Endpoint: GET /rooms/
Authentication: None
Response: 200 OK
[
{
"id": 1,
"name": "Living Room",
"project_id": 1,
"data": {},
"walls": [],
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}
]Returns a specific room by its ID.
Endpoint: GET /rooms/{room_id}
Authentication: None
Response: 200 OK
{
"id": 1,
"name": "Living Room",
"project_id": 1,
"data": {},
"walls": [],
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}Error Responses:
404 Not Found: Room not found
Updates a room's properties.
Endpoint: PUT /rooms/{room_id}
Authentication: None
Request Body:
{
"name": "Updated Room Name",
"data": {
"width": 600,
"height": 400
}
}Response: 200 OK
{
"id": 1,
"name": "Updated Room Name",
"project_id": 1,
"data": {
"width": 600,
"height": 400
},
"walls": [],
"created_at": "2025-10-05T12:00:00",
"updated_at": "2025-10-05T15:00:00"
}Error Responses:
404 Not Found: Room not found409 Conflict: Room name already exists
Deletes a room and all associated walls.
Endpoint: DELETE /rooms/{room_id}
Authentication: None
Response: 204 No Content
Error Responses:
404 Not Found: Room not found
Creates a new wall within a room.
Endpoint: POST /walls/
Authentication: None
Request Body:
{
"wall_id": "wall-front",
"room_id": 1,
"data": {
"version": 1,
"unit": "px",
"origin": "bottom-left",
"wall": {
"width": 620,
"height": 460,
"thickness": 15
},
"obstacles": []
}
}Response: 201 Created
{
"id": 1,
"wall_id": "wall-front",
"room_id": 1,
"data": {
"version": 1,
"unit": "px",
"origin": "bottom-left",
"wall": {
"width": 620,
"height": 460,
"thickness": 15
},
"obstacles": []
},
"motion_data": null,
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}Error Responses:
404 Not Found: Room not found409 Conflict: Wall ID already exists
Returns a wall by its public wall_id string.
Endpoint: GET /walls/by-wall-id/{wall_id}
Authentication: None
Response: 200 OK
{
"id": 1,
"wall_id": "wall-front",
"room_id": 1,
"data": {},
"motion_data": null,
"created_at": "2025-10-05T12:00:00",
"updated_at": null
}Error Responses:
404 Not Found: Wall not found
Updates a wall's properties by its database ID.
Endpoint: PUT /walls/{wall_id}
Authentication: None
Request Body:
{
"data": {
"version": 1,
"wall": {
"width": 700,
"height": 500
}
},
"motion_data": {
"strokes": [],
"params": {}
}
}Response: 200 OK
{
"id": 1,
"wall_id": "wall-front",
"room_id": 1,
"data": {
"version": 1,
"wall": {
"width": 700,
"height": 500
}
},
"motion_data": {
"strokes": [],
"params": {}
},
"created_at": "2025-10-05T12:00:00",
"updated_at": "2025-10-05T15:00:00"
}Error Responses:
404 Not Found: Wall not found409 Conflict: Wall ID already exists
Deletes a wall by its database ID.
Endpoint: DELETE /walls/{wall_id}
Authentication: None
Response: 204 No Content
Error Responses:
404 Not Found: Wall not found
Generates a motion path payload from wall data. Can accept either a wall_id to retrieve data from the database or direct wall_data. When using wall_id, the generated motion path is automatically saved to the wall's motion_data field.
Endpoint: POST /paths/generate-motion-path
Authentication: None
Request Body (Option 1 - Using wall_id):
{
"wall_id": "wall-front"
}Request Body (Option 2 - Using wall_data):
{
"wall_data": {
"version": 1,
"unit": "px",
"origin": "bottom-left",
"scale": {
"unitsPerPixel": 0.1
},
"wall": {
"id": "wall-front",
"name": "Front Wall",
"width": 620,
"height": 460,
"thickness": 15
},
"obstacles": [
{
"id": "window-1",
"name": "Door",
"corners": [[85, 246], [196, 246], [196, 0], [85, 0]]
}
]
}
}Coordinate System:
origin: Must be "bottom-left" (preferred) or "top-left"xcoordinates: 0 to wall.widthycoordinates: 0 to wall.height- For "top-left" origin, y=0 is at the top; the service converts to bottom-left internally
- Obstacles outside these ranges will be rejected with 400 error
Response: 200 OK
{
"strokes": [
{
"mode": "paint",
"points": [[10.0, 20.0], [30.0, 20.0]],
"metadata": {}
},
{
"mode": "move",
"points": [[30.0, 20.0], [30.0, 40.0]],
"metadata": {}
}
],
"geojson": {
"type": "FeatureCollection",
"features": []
},
"params": {
"step_size": 20.0,
"margin": 0.0,
"round_decimals": 3
}
}Error Responses:
400 Bad Request: Invalid wall data or coordinates404 Not Found: Wall ID not found (when using wall_id)
Error Response Example:
{
"detail": {
"code": "INVALID_WALL_DATA",
"message": "Invalid coordinate data",
"hints": [
"origin must be 'bottom-left' or 'top-left'",
"Use wall-local coordinates: 0 <= x <= wall.width",
"Use 0 <= y <= wall.height; do not send negative y",
"If using top-left, y=0 is the top; backend flips internally"
]
}
}Generates a motion path preview with summary statistics and metadata. Accepts the same input as generate-motion-path but returns summary information instead of the full payload.
Endpoint: POST /paths/preview-path
Authentication: None
Request Body: Same as /paths/generate-motion-path
Response: 200 OK
{
"total_strokes": 150,
"paint_strokes": 120,
"move_strokes": 30,
"total_points": 3000,
"bounds": {
"x_min": 0.0,
"x_max": 620.0,
"y_min": 0.0,
"y_max": 460.0
},
"parameters": {
"step_size": 20.0,
"margin": 0.0,
"round_decimals": 3
},
"strokes_preview": [
{
"mode": "paint",
"points": [[10.0, 20.0], [30.0, 20.0]],
"metadata": {}
}
],
"truncated": true
}Fields:
total_strokes: Total number of strokes in the motion pathpaint_strokes: Number of painting strokesmove_strokes: Number of movement strokes (non-painting)total_points: Total number of coordinate pointsbounds: Min/max coordinates of the pathparameters: Generation parameters usedstrokes_preview: First 5 strokes (for preview purposes)truncated: Whether the preview is truncated (more than 5 strokes exist)
Error Responses:
400 Bad Request: Invalid wall data or coordinates404 Not Found: Wall ID not found (when using wall_id)
All error responses follow a standard format:
{
"detail": "Error message describing what went wrong"
}For validation errors with multiple fields:
{
"detail": [
{
"loc": ["body", "field_name"],
"msg": "Field is required",
"type": "value_error.missing"
}
]
}
}200 OK: Request succeeded201 Created: Resource created successfully204 No Content: Resource deleted successfully400 Bad Request: Invalid request data401 Unauthorized: Authentication required or token invalid403 Forbidden: Authenticated but not authorized for this resource404 Not Found: Resource not found409 Conflict: Resource conflict (e.g., duplicate name)429 Too Many Requests: Rate limit exceeded
-
Authentication Flow:
- Call
POST /users/loginto get a JWT token - Store the token securely (e.g., localStorage, secure cookie)
- Include the token in all authenticated requests:
Authorization: Bearer <token> - Use
POST /users/refreshto get a new token before expiration - Use
GET /users/validate-tokento check if token is still valid
- Call
-
Typical Workflow:
- User logs in → receives JWT token
- Create a project →
POST /projects/ - Create rooms in the project →
POST /rooms/ - Create walls in each room →
POST /walls/ - Generate motion paths →
POST /paths/generate-motion-path - Retrieve project with all data →
GET /projects/{id}/with-rooms
-
Motion Path Generation:
- Use
wall_idwhen the wall already exists in the database - Use
wall_datafor testing or temporary calculations - Preview endpoint provides statistics without full data transfer
- Generated paths are automatically saved when using
wall_id
- Use
-
Data Flexibility:
datafields in Room and Wall models accept arbitrary JSON- Structure your data as needed by your application
- The API stores and retrieves it without modification
-
Hierarchical Deletion:
- Deleting a project deletes all its rooms and walls
- Deleting a room deletes all its walls
- Use with caution as these operations are irreversible