Uploads
Attach files (receipts, photos, PDFs, documents) to entities via the REST API
The Uploads API lets you programmatically attach files to entities in your workspace -- receipts on jobs, completed-work photos on invoices, signed PDFs on quotes, supporting documents on contacts, and so on. Files are stored in BlueSuite's secure storage and exposed through time-limited signed URLs (or permanent public URLs when you opt in).
A single namespace, /api/v1/uploads, handles attachments for every supported entity. You specify the parent entity by passing module_type and module_id.
Authentication
All endpoints require an API key with the appropriate scope. Pass your key in the Authorization header:
Authorization: Bearer wk_your_api_keyScopes
Upload endpoints inherit the parent entity's scope. To attach a file to a job, your key needs jobs:write. To list files on an invoice, it needs invoices:read. The wildcard read and write scopes work as well.
| Parent module | Read scope | Write scope |
|---|---|---|
contact | contacts:read | contacts:write |
request | requests:read | requests:write |
quote | quotes:read | quotes:write |
job | jobs:read | jobs:write |
invoice | invoices:read | invoices:write |
event | events:read | events:write |
timesheet | timesheets:read | timesheets:write |
File limits
- Max size: 5 MB per file
- Accepted MIME types:
image/jpeg,image/jpg,image/png,image/webp,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
The upload object
Every API response represents a link between a stored file and a parent entity. The same underlying file may be linked to several entities -- the id you work with in this API is the link's ID.
| Field | Type | Description |
|---|---|---|
link_id | integer | The link ID (use this with GET /:id, PATCH /:id, DELETE /:id) |
module_type | string | The parent entity type (e.g. job, invoice) |
module_id | integer | The parent entity ID |
name | string | Display name of the link |
description | string | Optional description on the link |
category | string | One of attachment, internal, public, expense |
bucket | string | workspaces (private) or workspaces_pub (public) |
path | string | Storage object key |
original_file_name | string | Original filename uploaded |
url | string | Time-limited signed URL (1 hour TTL) for private files, or a permanent public URL when bucket is workspaces_pub |
owner_id | string | null | UUID of the workspace member who owns the file |
meta | object | null | Image dimensions etc., when applicable |
link_created_at | string | ISO timestamp when the link was created |
link_modified_at | string | ISO timestamp when the link was last modified |
url is regenerated on every read, so always fetch the file before its TTL expires (one hour).
Endpoints
Upload a File
Uploads a file and links it to a parent entity. The request body must be multipart/form-data.
POST /api/v1/uploadsRequired Scope: <module>:write of the target entity (e.g. jobs:write to attach to a job)
Multipart fields:
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | The file to upload |
module_type | string | Yes | One of: contact, request, quote, job, invoice, event, timesheet |
module_id | integer | Yes | ID of the parent entity (must exist in your workspace) |
category | string | No | Default: attachment. One of: attachment, internal, public, expense |
name | string | No | Display name for the link. Default: the original filename |
description | string | No | Free-text description |
is_public | boolean | No | Default: false. When true, the file is stored in the public bucket and gets a permanent unauthenticated URL |
curl -X POST https://app.bluesuite.com/api/v1/uploads \
-H "Authorization: Bearer wk_your_api_key" \
-F "file=@receipt.pdf" \
-F "module_type=job" \
-F "module_id=42" \
-F "category=expense" \
-F "name=Home Depot receipt" \
-F "description=Materials for the kitchen install"Response (201 Created):
{
"success": true,
"data": {
"link_id": 815,
"module_type": "job",
"module_id": 42,
"name": "Home Depot receipt",
"description": "Materials for the kitchen install",
"category": "expense",
"bucket": "workspaces",
"path": "workspaces/123/home_depot_receipt-9f2a8c1e7b04.pdf",
"original_file_name": "receipt.pdf",
"url": "https://...supabase.co/storage/v1/object/sign/...?token=...",
"owner_id": "f7c19b34-5d2a-4e8f-9c6a-1234567890ab",
"meta": null,
"link_created_at": "2024-01-15T10:30:00.000Z",
"link_modified_at": "2024-01-15T10:30:00.000Z"
}
}List Uploads on an Entity
Returns the files linked to a specific parent entity, paginated newest-first.
GET /api/v1/uploads?module_type=:module_type&module_id=:module_idRequired Scope: <module>:read of the target entity
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
module_type | string | Yes | One of: contact, request, quote, job, invoice, event, timesheet |
module_id | integer | Yes | ID of the parent entity |
category | string | No | Filter by category: attachment, internal, public, expense |
page | integer | No | Default: 1 |
per_page | integer | No | Default: 25 (max 100) |
curl "https://app.bluesuite.com/api/v1/uploads?module_type=job&module_id=42" \
-H "Authorization: Bearer wk_your_api_key"Response:
{
"success": true,
"data": [
{
"link_id": 815,
"module_type": "job",
"module_id": 42,
"name": "Home Depot receipt",
"category": "expense",
"bucket": "workspaces",
"path": "workspaces/123/home_depot_receipt-9f2a8c1e7b04.pdf",
"url": "https://...supabase.co/storage/v1/object/sign/..."
}
],
"pagination": {
"page": 1,
"per_page": 25,
"total_pages": 1
}
}Get a Single Upload
Returns one upload link, including a fresh signed URL.
GET /api/v1/uploads/:id:id is the link_id returned by create or list.
Required Scope: <module>:read of the link's parent entity
curl https://app.bluesuite.com/api/v1/uploads/815 \
-H "Authorization: Bearer wk_your_api_key"Response: same shape as the create response.
Update Upload Metadata
Updates the name, description, and/or category of an existing link. The underlying file bytes cannot be replaced -- delete the link and upload a new file instead.
PATCH /api/v1/uploads/:idRequired Scope: <module>:write of the link's parent entity
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | New display name |
description | string | null | No | New description |
category | string | No | One of: attachment, internal, public, expense |
At least one field must be provided.
curl -X PATCH https://app.bluesuite.com/api/v1/uploads/815 \
-H "Authorization: Bearer wk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Home Depot receipt - Jan 15",
"category": "expense"
}'Response: the updated link (same shape as Get).
Delete an Upload
Removes the link between the file and the parent entity. If no other entities still link to the same file, the underlying file is also permanently deleted from storage.
DELETE /api/v1/uploads/:idRequired Scope: <module>:write of the link's parent entity
curl -X DELETE https://app.bluesuite.com/api/v1/uploads/815 \
-H "Authorization: Bearer wk_your_api_key"Response:
{
"success": true,
"data": {
"deleted": true
}
}Public vs. private files
By default, uploads land in the private bucket. The API returns a signed URL with a one-hour TTL on every read, so you must fetch (or copy) the file before the URL expires.
When is_public=true, the file is stored in the public bucket. The returned url is a permanent, unauthenticated URL suitable for embedding in emails, PDFs, or third-party tools. Only set is_public=true for assets you genuinely want publicly accessible.
Error Responses
{
"success": false,
"error": "Missing required scope: jobs:write"
}| Status | Description |
|---|---|
400 | Validation error (missing fields, invalid module_type, file too large or unsupported type) |
401 | Missing or invalid API key |
403 | API key lacks the required scope for the parent entity |
404 | Parent entity does not exist in your workspace, or the upload link was not found |
415 | Content-Type is not multipart/form-data (POST only) |
500 | Internal server error |
Validation errors include a details array describing each problem:
{
"success": false,
"error": "Validation error",
"details": [
{
"path": ["module_id"],
"message": "Required"
}
]
}Tips
- Always re-fetch the link (or list) just before downloading the file -- signed URLs expire after one hour.
- For receipts on jobs,
category: "expense"is the convention used elsewhere in BlueSuite. The dashboard recognises that category for expense reporting. nameis what shows up in the dashboard's Files tab. If you don't set it, the original filename is used.- One physical file can be linked to multiple entities. Deleting one link only removes the file when it was the last reference.