Photos & files
Upload, read, edit and delete job photos, and the documents on jobs and customers, with signed download links.
View as MarkdownEvery job has a photo gallery and a Files card. Photos carry GPS, the camera direction, a capture time, a stage (before, during, after) and tags, and feed photo reports and proposals. Files are documents: plans, permits, contracts, executed proposals. Customers (companies) can hold files too.
Uploads through the API behave like uploads in the app: phone photos are turned upright, thumbnails are made, EXIF GPS and capture time are read, photo uploads count toward the plan's photo allowance, and both show in the job timeline. They are recorded as the person who created the API key.
Note
Partner keys add firm_id to name the workspace: in the query string, or as a form or JSON field on writes. See Partner API.
Photos#
The photo object is the one GET /v1/jobs/{job_id}/photos returns (see Jobs): id, job_id, url, thumbnail_url, caption, title, stage, visibility (external or internal), include_in_report, taken_at, taken_by, lat, lng, compass_heading, tags, created_at, updated_at. Photo timestamps are UTC without a timezone suffix.
internal photos are team only. They never appear in customer reports, so include_in_report is always false for them.
Upload a photo#
POST /v1/jobs/{job_id}/photos
Permission: Write jobs
Send the image as multipart form field photo, or send JSON with a url for TruTec to download. One photo per request.
| Name | Type | Required | Description |
|---|---|---|---|
photo |
file | One of photo / url |
JPEG, PNG, WebP, GIF or HEIC (or a phone video). Multipart only. |
url |
string | One of photo / url |
JSON only. A public https:// link to the image file itself, up to 15 MB. Links to private networks are refused. |
filename |
string | No | With url: the name to store. The extension follows the image type. |
caption |
string | No | Description. |
title |
string | No | Short label, up to 100 characters. |
stage |
string | No | before, during, after or general (default). |
visibility |
string | No | external or internal. Default: the workspace's photo setting. |
include_in_report |
boolean | No | Default: the workspace's photo setting. Ignored for internal photos. |
latitude, longitude |
number | No | Where it was taken. GPS in the file's EXIF wins. Send both or neither. |
compass_heading |
number | No | Camera direction, degrees clockwise from north. |
taken_at |
string | No | ISO 8601 capture time. Default: the file's EXIF time, else now. |
tag_ids |
array of integers | No | Photo tag ids from List photo tags. In multipart, repeat the field or send 19,22. |
client_uid |
string | No | Your id for this shot, up to 64 characters. Sending the same client_uid again returns the photo already uploaded (200, "created": false) instead of a duplicate. Use it to make retries safe. |
curl -X POST https://trutec.ai/api/v1/jobs/4812/photos \
-H "X-API-Key: sk_your_api_key_here" \
-F "photo=@IMG_2201.jpg" \
-F "caption=Edge cracking by the north entrance" \
-F "stage=before" \
-F "tag_ids=19" \
-F "client_uid=crew-tablet-7-0412"
curl -X POST https://trutec.ai/api/v1/jobs/4812/photos \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://cdn.example.com/site/IMG_2201.jpg", "stage": "before", "caption": "Edge cracking by the north entrance"}'
Returns 201:
{
"data": {
"id": 88231,
"job_id": 4812,
"url": "https://media.example.com/photos/job_4812/a81c2f.jpg",
"thumbnail_url": "https://media.example.com/photos/job_4812/a81c2f_thumb.jpg",
"caption": "Edge cracking by the north entrance",
"title": null,
"stage": "before",
"include_in_report": true,
"visibility": "external",
"taken_at": "2026-10-12T14:58:02",
"taken_by": {"id": 311, "name": "Dana Ortiz", "username": "dana", "email": "[email protected]"},
"lat": 30.40121,
"lng": -97.72088,
"compass_heading": 42.0,
"tags": [{"id": 19, "name": "Cracking", "color": "#ef4444"}],
"created_at": "2026-10-12T15:04:11.204511",
"updated_at": "2026-10-12T15:04:11.204517"
},
"created": true
}
Captions may be filled in automatically a few seconds later if the workspace has automatic photo descriptions turned on.
| Status | error |
|---|---|
| 400 | No photo provided, One photo per request, Send a file or a url, not both, Invalid photo (unsupported file type), a field message such as stage must be one of: ..., or Unknown fields |
| 403 | photo_limit_reached with photos_used, photos_limit and photos_remaining, when the plan's photo allowance is used up |
| 404 | Job not found, Photo tag not found |
| 422 | url is not an image, Could not fetch url, Image too large |
| 503 | Photo storage is temporarily unavailable with "retryable": true. Retry with the same client_uid. |
List photos#
GET /v1/photos
Permission: Read jobs
Every job photo in the workspace. Use it with updated_after to keep a copy of all photos in sync; for one job, GET /v1/jobs/{job_id}/photos returns the gallery order.
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
integer | No | Only this job. |
stage |
string | No | before, during, after or general. |
visibility |
string | No | external or internal. |
updated_after |
string | No | ISO 8601. Photos changed after it, oldest first. Without it, newest first. |
limit |
integer | No | Default 50, maximum 200. |
offset |
integer | No | Default 0. |
curl "https://trutec.ai/api/v1/photos?updated_after=2026-10-11T00:00:00Z&limit=200" \
-H "X-API-Key: sk_your_api_key_here"
Returns {"data": [photo, ...], "pagination": {"total", "limit", "offset"}}.
Get a photo#
GET /v1/photos/{photo_id}
Permission: Read jobs
Returns {"data": photo}. A photo in another workspace is 404 Photo not found.
Update a photo#
PATCH /v1/photos/{photo_id}
Permission: Write jobs
Send only the fields that change: caption, title, stage, visibility, include_in_report, tag_ids (replaces the tags), latitude, longitude, compass_heading (wrapped into 0–359; null clears it) and sort_order. Setting visibility to internal also clears include_in_report. A location or direction you set here is kept even if the file's own GPS says otherwise.
curl -X PATCH https://trutec.ai/api/v1/photos/88231 \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"caption": "Raveling at the dumpster pad", "stage": "after", "tag_ids": [19, 22]}'
Returns 200 with {"data": photo}. Errors: 400 for a bad value or unknown field, 404 Photo tag not found for a tag from another workspace.
Delete a photo#
DELETE /v1/photos/{photo_id}
Permission: Write jobs and Delete records
Deletes the photo and its stored files, unless the job's site assessment still shows the same file, and logs it on the job. Returns 204. This cannot be undone.
List photo tags#
GET /v1/photo-tags
Permission: Read jobs
{
"data": [
{"id": 19, "name": "Cracking", "color": "#ef4444", "description": "Alligator, block, edge and transverse cracking."},
{"id": 22, "name": "Drainage", "color": "#10b981", "description": null}
]
}
Files#
The file object#
| Field | Type | Description |
|---|---|---|
id |
integer | File id. |
filename |
string | The name it was uploaded with. |
content_type |
string or null | MIME type. |
size_bytes |
integer or null | Size. |
description |
string or null | |
document_type |
string or null | Set for files TruTec made, for example executed_proposal or executed_agreement. |
source |
string | job, customer, contact, library (Company Library), proposal or work_order. |
audience |
string | office, or field when crews see it in the field app and dispatch packets. |
customer_visible |
boolean | Shown on the customer's portal. |
job_id, client_account_id, contact_id, work_order_id |
integer or null | What the file belongs to. |
folder |
object or null | Company Library folder {id, name}. |
uploaded_by |
object | {id, name}. |
download_url |
string | API path that redirects to the file. Needs your API key. |
signed_url, signed_url_expires_at |
string | Only on Get a file: a direct link that works without a key for one hour. |
attachment |
object | Only on Company Library files in a job's list: {id, job_id, attached_by_rule, created_at}. |
created_at |
string | UTC, ISO 8601 with Z. |
List a job's files#
GET /v1/jobs/{job_id}/files
Permission: Read jobs
The job's own files, newest first, then Company Library files linked to the job (insurance certificates, spec sheets), the same list as the job page's Files card.
curl https://trutec.ai/api/v1/jobs/4812/files \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{
"id": 5531,
"filename": "civil-set-rev3.pdf",
"content_type": "application/pdf",
"size_bytes": 4821933,
"description": "Civil set, revision 3",
"document_type": null,
"source": "job",
"audience": "field",
"customer_visible": false,
"job_id": 4812,
"client_account_id": null,
"contact_id": null,
"work_order_id": null,
"folder": null,
"uploaded_by": {"id": 311, "name": "Dana Ortiz"},
"download_url": "/api/v1/files/5531/download",
"created_at": "2026-10-12T15:04:11Z"
}
],
"pagination": {"total": 1, "limit": 1, "offset": 0}
}
List files#
GET /v1/files
Permission: Read jobs
Every file in the workspace, newest first.
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
integer | No | Files on this job (not linked library files). |
client_account_id |
integer | No | Files on this customer. |
contact_id |
integer | No | Files on this contact. |
source |
string | No | job, customer, contact, library, proposal or work_order. |
q |
string | No | Search file name and description. |
created_after |
string | No | ISO 8601. Files added after it, oldest first. Files have no updated_at; use this to pick up new files. |
limit, offset |
integer | No | Default 50 (max 200) and 0. |
Upload a file to a job#
POST /v1/jobs/{job_id}/files
Permission: Write jobs
Multipart upload. One file per request, up to 200 MB: PDF, Word, Excel, PowerPoint, text, CSV, RTF, images, ZIP, RAR or 7z.
| Name | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | The document. |
description |
string | No | |
audience |
string | No | office (default) or field to share it with crews. |
customer_visible |
boolean | No | Show it on the customer portal. The job must be linked to a customer whose portal is turned on. |
curl -X POST https://trutec.ai/api/v1/jobs/4812/files \
-H "X-API-Key: sk_your_api_key_here" \
-F "[email protected]" \
-F "description=Civil set, revision 3" \
-F "audience=field"
Returns 201 with {"data": file}. The upload is logged on the job and copied to the workspace's connected cloud storage and CRM, as uploads in the app are.
| Status | error |
|---|---|
| 400 | No file provided, Upload with multipart/form-data, Invalid file (type or size), audience must be office or field |
| 404 | Job not found |
| 409 | Customer portal not ready, with a message saying what to turn on and portal_status (job_not_linked, account_archived or portal_disabled) |
| 503 | File storage is temporarily unavailable. Retry. |
Upload a file to a customer#
POST /v1/client-accounts/{account_id}/files
Permission: Write jobs
The same multipart upload onto a customer record: W-9s, certificates of insurance, master agreements. Takes file and description. Returns 201 with "source": "customer".
curl -X POST https://trutec.ai/api/v1/client-accounts/915/files \
-H "X-API-Key: sk_your_api_key_here" \
-F "[email protected]" -F "description=W-9"
Get a file#
GET /v1/files/{file_id}
Permission: Read jobs
Returns the file with signed_url, a direct link you can hand to a browser or another system, valid for one hour.
{
"data": {
"id": 5531,
"filename": "civil-set-rev3.pdf",
"source": "job",
"job_id": 4812,
"download_url": "/api/v1/files/5531/download",
"signed_url": "https://media.example.com/documents/job_4812/civil-set-rev3.pdf?expires=1760285051&signature=...",
"signed_url_expires_at": "2026-10-12T16:04:11Z",
"created_at": "2026-10-12T15:04:11Z"
}
}
Download a file#
GET /v1/files/{file_id}/download
Permission: Read jobs
Redirects (302) to a short-lived link to the file. Tell your client to follow redirects:
curl -L -o civil-set-rev3.pdf https://trutec.ai/api/v1/files/5531/download \
-H "X-API-Key: sk_your_api_key_here"
Update a file#
PATCH /v1/files/{file_id}
Permission: Write jobs
Change description, and for job files audience and customer_visible. Sending audience or customer_visible for a file that is not on a job returns 400. Turning on customer_visible follows the same portal rule as uploads (409).
curl -X PATCH https://trutec.ai/api/v1/files/5531 \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"audience": "field"}'
Delete a file#
DELETE /v1/files/{file_id}
Permission: Write jobs and Delete records
Deletes the file and its stored copy and returns 204. A Company Library file is also removed from every job it was linked to. Job file deletes are logged on the job. This cannot be undone.
Photo tags#
Add, change and remove the workspace's photo tags, as the photo gallery does. List them with List photo tags.
Create a photo tag#
POST /v1/photo-tags
Permission: Write jobs
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Up to 100 characters. |
color |
string | No | Hex color like #ef4444. Default gray. |
description |
string | No | What the tag means. The photo AI uses it to know what to look for. |
curl -X POST https://trutec.ai/api/v1/photo-tags \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"name": "Raveling", "color": "#f97316", "description": "Loose aggregate and a rough, worn surface."}'
Returns 201 with {"data": {"id": 31, "name": "Raveling", "color": "#f97316", "description": "..."}}. A tag with the same name returns 409 duplicate_name with its id in conflicting_id.
Update a photo tag#
PATCH /v1/photo-tags/{tag_id}
Permission: Write jobs
Send any of name, color (null sets the default gray) and description (null or "" clears it). Photos keep the tag. Returns 200 with {"data": tag}; 409 duplicate_name when another tag has the new name.
Delete a photo tag#
DELETE /v1/photo-tags/{tag_id}
Permission: Write jobs and Delete records
Deletes the tag and takes it off every photo that has it; the photos stay. Returns 204. This cannot be undone.