Resources

Photos

Photos and videos captured against a project: inspection sets, progress shots, damage documentation, and anything a customer uploaded through the portal.

GET/v1/photos

List photos

Newest first. Requires projects.view on the member who created the API key.

Query parameters

NameTypeDescription
project_iduuidOnly photos on this project.
created_afterdatePhotos created on or after this YYYY-MM-DD date.
created_beforedatePhotos created on or before this YYYY-MM-DD date. Inclusive of the whole named day.
media_typeenumimage or video.
content_hashstringHex SHA-256 of the file bytes. Use it to ask whether a specific file is already here.
include_trashedbooleanSet true to include photos in the trash. Excluded by default.
limitintegerRows per page, 1 to 100. Defaults to 25.
offsetintegerRows to skip. Combine with has_more to page.

Only photos whose file exists are returned

A photo row can exist before its bytes do. The mobile app reserves a row and hands the transfer to a background upload, so there is a window where the record is real and the file is not. This endpoint excludes those, and photo.uploaded waits for the same moment, so anything you receive here or by webhook is always fetchable.

GET/v1/photos/{id}

Retrieve a photo

Returns one photo with freshly signed links, plus trashed_at. Unlike the list, this does return a trashed photo, so an id from an earlier webhook tells you what became of it rather than 404ing as though it never existed.

The photo object

FieldTypeDescription
iduuidThe photo id.
project_iduuidThe project it belongs to.
namestringDisplay name, falling back to the original filename.
descriptionstringCaption a member added, if any.
media_typeenumimage or video.
width / heightintegerPixel dimensions of the original, when known.
size_bytesintegerSize of the original file.
content_hashstringSHA-256 of the bytes. Null for photos uploaded before the column existed.
from_customerbooleanTrue when the end customer uploaded it through the portal rather than a workspace member.
visible_in_customer_portalbooleanWhether the photo is shared into the customer-facing gallery.
uploaded_byuuidThe workspace member who uploaded it. Null for portal uploads.
urls.originalstringSigned link to the full-resolution file.
urls.displaystringSigned link to the web-sized rendition. Null until it has been generated.
urls.thumbnailstringSigned link to the thumbnail. Null until it has been generated.
urls.expires_atstringWhen the three links above stop working.

Links expire

Every link is signed and valid for 24 hours, and needs no authentication to fetch. Treat one like a password: anyone holding it can read the file until it expires. Store the photo id rather than the URL, and re-fetch when you need the bytes.

Webhook payloads are built at delivery time, so each retry carries a fresh link rather than an expired one from the first attempt.

Mirroring photos elsewhere

Unlike expenses, photos need no reconciliation write-back. Both systems hold the same file, so content_hash lets you compare the two sides directly: list what is here, list what is there, and copy the difference. Nothing to keep in sync and nothing to reset, and it self-corrects when somebody uploads by hand at the far end.

See webhooks to be told the moment a photo lands instead of polling, and pagination and rate limits before writing a sweep.