diff --git a/domain-skills/buildertrend/api.md b/domain-skills/buildertrend/api.md new file mode 100644 index 00000000..a6843bf9 --- /dev/null +++ b/domain-skills/buildertrend/api.md @@ -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//Grid` (e.g. `/api/messages/Grid`) and ad-hoc GETs + (`/api/Messages?messageId=...`). +- Newer: `POST /apix/v2//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: , 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: ""}`; + 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=&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 `` 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.