Skip to content
TruTecDevelopers
Get an API key

Photos & files

Upload, read, edit and delete job photos, and the documents on jobs and customers, with signed download links.

View as Markdown

Every 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.