Salesforce report 00O1K00000A7UGDUA3 is the source of truth. This module executes
the saved report with includeDetails=true and derives every column from report
metadata. It never creates, updates or deletes Salesforce records or reports.
GET /v6/reports/sales: authenticated human Administrator or Talent Manager.GET /v6/reports/win/sales: machine token withreports:sales. Human administrator tokens andreports:allalone do not grant access.- Register
reports:saleson the identity provider's API resource and grant it to the WIN client before requesting a client-credentials token. Never place a WIN machine credential or Salesforce secret in the browser.
Both routes use the existing JWT authentication middleware, then independent
role/scope checks. Responses use Cache-Control: private, no-store.
| Environment variable | Value |
|---|---|
SALESFORCE_API_CONSUMER_KEY |
Required connected-app consumer key, injected from secret storage |
SALESFORCE_API_CONSUMER_SECRET |
Required connected-app consumer secret, injected from secret storage |
SALESFORCE_LOGIN_URL |
Default https://topcoder.my.salesforce.com |
SALESFORCE_API_VERSION |
Default 65.0, without the v prefix |
SALESFORCE_SALES_REPORT_ID |
Default 00O1K00000A7UGDUA3 |
Enable the connected app's OAuth Client Credentials Flow and configure a Run
As user with API access, permission to run reports, report-folder access, and
access to the report's underlying objects/fields. The OAuth error no client credentials user enabled means this Run As setup is missing. The app starts
without these settings, but Sales requests return 503 until configured.
For the current dev deployment convention, inject the variables from secure SSM
parameters under /config/reports-api-v6/appvar/. Do not commit real values.
Both endpoints accept the same query parameters:
| Parameter | Meaning |
|---|---|
page, perPage |
One-based page (default 1); page size 1–200 (default 25) |
search |
Case-insensitive substring across all displayed cells, up to 200 characters |
filterColumn, filterValue |
Column ID and case-insensitive displayed-value substring; supply both |
drilldownColumn, drilldownValue |
Column ID and exact, case-insensitive displayed value; narrows the returned page only, never summary; supply both |
sortBy, sortOrder |
Column ID and asc/desc; numeric and ISO date values sort before pagination |
dateColumn |
Column ID of a date/datetime column, such as Created Date or Close Date |
dateFrom, dateTo |
Inclusive YYYY-MM-DD bounds; either or both, and both require dateColumn |
refresh |
true to refresh, subject to the one-minute minimum interval; default false |
Response fields: reportId, reportName, columns[{id,label,dataType}],
rows[{id,cells:[{label,value,currencyCode?}]}], allData, sourceRowCount,
total, page, perPage, totalPages, refreshedAt, refreshAfterSeconds,
summary.
Cells follow column order. Labels are plain text, never HTML. Currency values
retain their amount and currency code. Null values are preserved. Row IDs are
snapshot-local fact-map keys, not durable Salesforce record identifiers.
Grouping-only fields, including Stage in Bookings By Stage, precede the detail
columns and support the same filtering and sorting. Lookup names sort by their
displayed labels, while dates and currency amounts use underlying typed values.
HTML formulas are projected to text; Forecast Alert uses its image's alt label
without fetching a protected Salesforce image.
Filtering and sorting operate over the complete received snapshot, before
pagination. total is the returned received-row count, after any drilldown;
sourceRowCount is its unfiltered count. Out-of-range pages clamp to the final
available page. Empty reports return zero rows and totalPages: 0, page: 1.
drilldownColumn/drilldownValue narrow rows, total and totalPages to the
rows whose displayed value in that column equals drilldownValue exactly, ignoring
case and surrounding whitespace. Unlike filterColumn/filterValue it is applied
after aggregation, so summary continues to describe the whole filtered set.
That lets a dashboard drill into one summary.groups[].buckets[] entry — a pipeline
stage, say — while every bucket stays on screen to be clicked next. A drilldown
therefore makes summary.recordCount larger than total. Supplying one half of the
pair, or a column ID the report does not define, returns 400.
dateColumn selects which date the range applies to, so the same report answers
both pipeline generation (Created Date) and revenue realization (Close Date)
questions. Bounds are inclusive and combine with search and
filterColumn/filterValue. Selecting a dateColumn with no bound is a no-op,
which lets a client keep the field selected while the range is empty.
Comparison uses each cell's underlying Salesforce value, never its localized
label: date and datetime values arrive as ISO 8601, and a datetime keeps the
report's own offset, so its day matches the day the report displays. A row whose
date cell is null or unparseable cannot satisfy a range and is excluded rather
than counted. 400 responses cover a bound without dateColumn, dateFrom
after dateTo, a dateColumn that is not a date/datetime column, and any
bound that is not a real YYYY-MM-DD calendar day (2026-02-30 and non-leap
2027-02-29 are rejected; datetimes and offsets are not accepted as bounds).
summary describes every matching row in the snapshot, not the returned
page, so counts and totals stay correct under pagination:
| Field | Meaning |
|---|---|
recordCount |
Matching rows; equal to total unless a drilldown narrows the page |
amounts[] |
One entry per currency/double column: columnId, label, total, contributing count, and currencyCode when the contributing rows agree |
groups[] |
Up to three picklist/multipicklist/combobox/boolean columns broken into buckets[{label,count,total,amounts[]}], ordered by total then count, capped at 25 with the remainder in otherBuckets |
Bucket total uses the report's first amount column, named in amountColumnId,
while buckets[].amounts[] repeats every amount column inside the bucket using the
same entry shape and order as summary.amounts, so a breakdown can show a stage's
Amount beside its Expected Revenue.
Totals round to cents so repeated floating-point addition cannot leak artifacts
into displayed currency. A currencyCode is omitted when contributing rows
declare different currencies; rows that declare none cannot contradict the rest.
Because aggregates cover received rows only, allData: false limits them
exactly as it limits total.
Salesforce Analytics limits detail responses to 2,000 rows. allData: false
explicitly flags an incomplete upstream snapshot; the UI warns that search,
filtering and counts apply only to returned rows. It must never be treated as a
complete export by WIN. Refine the saved Salesforce report if the limit is hit;
this API does not replace report semantics with a guessed SOQL query. Joined
reports and reports without details are rejected. See Salesforce's
Reports API limits
and report execution contract.
One in-memory snapshot per service instance lasts 60 seconds. Concurrent reads share an in-flight request; manual refresh uses the same one-minute interval. No report data is persisted. A failed refresh returns an error, with a one-minute retry cooldown, and never changes the last successful timestamp. The UI refreshes visible pages every minute and on return to a visible tab; hidden tabs do not poll. It displays stale-data status when a refresh fails.
OAuth and report requests time out after 15 seconds per attempt. Network failures, HTTP 429 and 5xx retry up to three attempts with bounded backoff; 401 report responses renew OAuth once. A Salesforce synchronous report quota error pauses report calls from that instance for five minutes. Warnings include the HTTP status, bounded Salesforce error code/message, configured report ID, and Salesforce request ID when present; tokens and full upstream response bodies are omitted. Validation returns 400, missing configuration 503, and upstream failures 502. Authorization returns 401/403 before Salesforce is contacted.
Future reports can reuse SalesforceReportsClient.runReport(reportId) and the
metadata normalization pattern. Add explicit server-side report selection and
authorization for each; do not accept arbitrary report IDs under the sales scope.
Run nvm use, pnpm lint, pnpm build and pnpm test --runInBand.