API reference
Base URL https://trace.foundrcode.com/api/v1. JSON in, JSON out. Lists are paginated with
?page= and return a meta block. Timestamps are ISO 8601 in UTC.
Session
/api/v1/auth/login
Exchange credentials and a device identifier for a bearer token.
/api/v1/auth/logout
Revoke the token making the request.
/api/v1/auth/me
The signed-in user, their role, and the team the token is scoped to.
/api/v1/auth/switch-team
Move the session to another team the user belongs to.
Sync
/api/v1/bootstrap
projects:read
Everything a device needs on first run: projects with coordinates, tags, checklists and team settings, in one request.
/api/v1/devices/heartbeat
Report queue depth and app version. This is what fills the "devices with a backlog" panel in the office.
Projects
/api/v1/projects
projects:read
Active projects, newest activity first. Filter with ?q= and ?status=.
/api/v1/projects/nearby
projects:read
Projects within range of a coordinate. Takes lat, lng and accuracy_m, and returns the same match reasoning the app uses.
/api/v1/projects
projects:write
Create a project. Coordinates are optional; without them the project can never be auto-matched.
/api/v1/projects/{project}
projects:read
One project, with counts.
Photos
/api/v1/photos
photos:read
Photos, filterable by project, tag, date range and assignment source.
/api/v1/photos
photos:write
Upload. Multipart, and idempotent on local_capture_id — a repeat returns 200 with duplicate:true rather than a second row.
/api/v1/photos/{photo}
photos:read
One photo with its URLs, tags, GPS and assignment reasoning.
/api/v1/photos/{photo}
photos:write
Edit the caption, retag, or move it to another project. Capture time and GPS are read-only: they are evidence.
Before & after
/api/v1/photo-groups
photos:read
Paired captures, with both halves resolved.
/api/v1/photo-groups
photos:write
Pair two photos as a before and an after.
Checklists
/api/v1/checklists
checklists:read
Checklists on the jobs this user can see. ?mine=1 narrows to their own assignments.
/api/v1/checklists/{checklist}
checklists:read
One checklist with its items and their evidence.
/api/v1/checklist-items/{item}
checklists:write
Tick an item off. An item marked requires_photo is refused with a 422 until one is attached.
Tasks
/api/v1/tasks
tasks:read
Tasks, filterable by project, status (open, done, overdue, all) and ?mine=1.
/api/v1/tasks/{task}
tasks:read
One task, with the photo it points at.
/api/v1/tasks
tasks:write
Raise a task. Naming a photo_id is enough — it implies the job it was taken on.
/api/v1/tasks/{task}
tasks:write
Edit, reassign, or set completed to close it. Closing records who and when.
/api/v1/tasks/{task}
tasks:write
Withdraw a task.
Pages
/api/v1/pages
pages:read
Pages, filterable by project and searchable with ?q=. Bodies are omitted from the list.
/api/v1/pages/{page}
pages:read
One page, returned both as Markdown source and as already-escaped HTML.
/api/v1/pages
pages:write
Write a page.
/api/v1/pages/{page}
pages:write
Edit one.
Customers
/api/v1/customers
customers:read
The customer list, searchable with ?q= across name, company, email, phone and city.
/api/v1/customers/{customer}
customers:read
One customer, with their additional contacts.
/api/v1/customers
customers:write
Create a customer — usually the direction that matters, since the CRM already knows who they are.
Comments
/api/v1/comments
comments:read
A thread on a photo or a project.
/api/v1/comments
comments:write
Post a comment. @mentions of teammates are resolved at write time.
Reports
/api/v1/reports
reports:read
Reports, with their status and share state.
/api/v1/reports/{report}
reports:read
One report, its photos in order, and the download URL when it has rendered.
A photo, in full
{
"id": 8412,
"local_capture_id": "5f2c9d18-3a6e-4b71-9e2f-70d1c2a4b8e3",
"project": { "id": 91, "name": "Harlan Street" },
"captured_at": "2026-08-19T14:02:11Z",
"uploaded_at": "2026-08-19T18:40:02Z",
"media_type": "photo",
"description": "Flashing pulled away from the stack.",
"tags": ["Before", "Roof"],
"location": {
"lat": 39.7392,
"lng": -104.9903,
"accuracy_m": 8
},
"assignment": {
"source": "gps",
"distance_m": 22,
"reason": null
},
"urls": {
"full": "https://…/photos/8412/file",
"thumb": "https://…/photos/8412/file?size=thumb",
"annotated": null
},
"size_bytes": 2841100
}
A page, in full
Pages come back twice: body as the author typed it, for anything that wants
to edit or re-render it, and body_html already run through the same escaping
path the web uses. An integrator embedding a scope in their own system should not have to
reimplement — or forget — the sanitising.
{
"id": 44,
"title": "Scope of work",
"excerpt": "What was agreed Strip the front elevation back to the deck…",
"project": { "id": 91, "name": "Harlan Street" },
"updated_by": "Nadia Fischer",
"updated_at": "2026-08-19T09:14:02Z",
"body": "## What was agreed\n\nStrip the front elevation…",
"body_html": "<h2>What was agreed</h2>\n<p>Strip the front elevation…</p>"
}
assignment.source is one of gps,
manual, corrected or unassigned. When it is
unassigned, reason says why the matcher declined —
ambiguous, low_accuracy, outside_radius or
no_fix. It never guesses, and it never returns a match it is not sure of.