Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions domain-skills/buildertrend/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Buildertrend — internal API map

Construction PM SaaS at `buildertrend.net`. The SPA talks to two API generations,
both same-origin cookie-authed (`credentials: 'include'`), so the fastest path is
`fetch` from page context (`js(...)` / `page.evaluate`) after login.

## Login

- `https://buildertrend.net/app/...` redirects to `login.buildertrend.com/u/login` (Auth0
universal login) when the session is gone. Plain email + password form
(`input[name=username]`, `input[name=password]`), Enter submits, then it bounces back to
the original `/app/` URL. No MFA observed on this tenant, but treat an unexpected second
screen as an auth wall.
- Sessions do expire — never assume "user is logged in" survives across days.

## API generations

- Legacy: `POST /api/<Entity>/Grid` (e.g. `/api/messages/Grid`) and ad-hoc GETs
(`/api/Messages?messageId=...`).
- Newer: `POST /apix/v2/<Entity>/grid` (e.g. `/apix/v2/DailyLogs/grid`).
- Both: JSON in/out, paginated via a `pagingData` object, rows newest-first, so
incremental scrapes can early-stop on the first already-seen row.

## Daily Logs

`POST /apix/v2/DailyLogs/grid`

```json
{
"jobIds": [44319042, ...], // REQUIRED — empty array returns 0 rows
"filters": {
"3": "", "4": "",
"8": "{\"SelectedValue\":2147483647,\"StartDate\":null,\"EndDate\":null}", // all-time
"10": "", "11": 0
},
"gridRequest": { "hideMultiJobsColumns": true, "emptyStateEntity": 18 },
"pagingData": { "pageNumber": 1, "pageSize": 20, "resetScroll": false,
"firstRow": 1, "lastRow": 20, "totalRowsAllPages": 0, "currentPage": 1 }
}
```

- Response: `{ data: [...], records: <total>, totalPages, ... }`.
- Row fields worth knowing: `dailyLogId`, `externalId` (uuid), `jobsiteId`, `jobsiteName`,
`addedById`/`addedBy` (author display name), `updatedById`, `logDate`, `logTitle`,
`logNotes` (plain text), `tags[]`, `viewableBy {subs, owner, internalUsers}`,
`coordinate {latitude, longitude}`, `weatherInformation {...}`,
`attachedFiles.files[]` (with `thumbnail` / `docPath` URLs), `discussionLink.commentCount`.
- The job-ids list comes from the job picker; the UI posts the full set to
`/api/jobpicker/SetJobPickerData` when you click "All N Open Jobs", but the grid call
itself carries `jobIds` explicitly — you don't need the picker call at all.
- Date filter: key `"8"` with `SelectedValue: 2147483647` = all time. Custom-range
`SelectedValue`/date formats were not cracked (naive `StartDate`/`EndDate` guesses return
0 rows) — if you need a window, capture the request after setting the filter in the UI,
or just rely on newest-first + early-stop.

## Jobs list / export

- UI: `https://buildertrend.net/app/Jobs/List`. Grid columns include Job Name, address,
and Project Manager.
- "Export all" (share icon, top right) → `POST /api/Jobsites/ExportToExcel` with
`{gridRequest: {selectedColumns: [...], sortColumn, ...}, filters: "<json-string>"}`;
the response downloads a `Jobsites.xls`. Replayable from page context for headless
export — capture a live payload once and reuse it.

## Messages (already tooled elsewhere)

- `POST /api/messages/Grid` (legacy shape, `pageSize` up to 200 works) and detail
`GET /api/Messages?messageId=<id>&folderId=-9`. A full incremental Playwright scraper
exists in the Next Modular repo at `tools/builder-trend/scrape-messages-api/`.

## Traps

- `jobIds: []` on DailyLogs grid silently returns `records: 0` — not an error.
- `messageDate` on messages has no tz offset; only `messageDateForMobile` is tz-aware.
- SPA log titles render as `<a>` without per-log hrefs — there is no scrape-able deep link
in the DOM; reconstruct links from ids if needed.
- An analytics `mp-proxy/track` POST fires on most clicks — ignore it when sniffing.