BlueSuite API
REST API v1

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_key

Scopes

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 moduleRead scopeWrite scope
contactcontacts:readcontacts:write
requestrequests:readrequests:write
quotequotes:readquotes:write
jobjobs:readjobs:write
invoiceinvoices:readinvoices:write
eventevents:readevents:write
timesheettimesheets:readtimesheets: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.

FieldTypeDescription
link_idintegerThe link ID (use this with GET /:id, PATCH /:id, DELETE /:id)
module_typestringThe parent entity type (e.g. job, invoice)
module_idintegerThe parent entity ID
namestringDisplay name of the link
descriptionstringOptional description on the link
categorystringOne of attachment, internal, public, expense
bucketstringworkspaces (private) or workspaces_pub (public)
pathstringStorage object key
original_file_namestringOriginal filename uploaded
urlstringTime-limited signed URL (1 hour TTL) for private files, or a permanent public URL when bucket is workspaces_pub
owner_idstring | nullUUID of the workspace member who owns the file
metaobject | nullImage dimensions etc., when applicable
link_created_atstringISO timestamp when the link was created
link_modified_atstringISO 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/uploads

Required Scope: <module>:write of the target entity (e.g. jobs:write to attach to a job)

Multipart fields:

FieldTypeRequiredDescription
filefileYesThe file to upload
module_typestringYesOne of: contact, request, quote, job, invoice, event, timesheet
module_idintegerYesID of the parent entity (must exist in your workspace)
categorystringNoDefault: attachment. One of: attachment, internal, public, expense
namestringNoDisplay name for the link. Default: the original filename
descriptionstringNoFree-text description
is_publicbooleanNoDefault: 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_id

Required Scope: <module>:read of the target entity

Query Parameters:

ParameterTypeRequiredDescription
module_typestringYesOne of: contact, request, quote, job, invoice, event, timesheet
module_idintegerYesID of the parent entity
categorystringNoFilter by category: attachment, internal, public, expense
pageintegerNoDefault: 1
per_pageintegerNoDefault: 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/:id

Required Scope: <module>:write of the link's parent entity

Request Body:

FieldTypeRequiredDescription
namestringNoNew display name
descriptionstring | nullNoNew description
categorystringNoOne 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/:id

Required 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"
}
StatusDescription
400Validation error (missing fields, invalid module_type, file too large or unsupported type)
401Missing or invalid API key
403API key lacks the required scope for the parent entity
404Parent entity does not exist in your workspace, or the upload link was not found
415Content-Type is not multipart/form-data (POST only)
500Internal 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.
  • name is 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.

On this page