Portfolio
Every domain in the workspace at once, and actions across many of them.
- GET/v1/portfolio
- POST/v1/portfolio/bulk/apply
- POST/v1/portfolio/bulk/preview
- GET/v1/portfolio/clients/{groupId}/report
- GET/v1/portfolio/clients/{groupId}/report.pdf
- PUT/v1/portfolio/clients/{groupId}/schedule
- GET/v1/portfolio/views
- POST/v1/portfolio/views
- DELETE/v1/portfolio/views/{id}
- PUT/v1/portfolio/views/{id}
/v1/portfolioEvery domain you can see, with score, leaks, last scan, publish state and consent
One row per domain in your scope. A member limited to some clients sees only those clients’ domains; the database itself enforces this. Sorting, filtering and paging happen on the server. leaks and consentRate are null when unknown — never zero — and sort last either way.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| q | query | string | Case-insensitive hostname search. |
| groupId | query | string | A client (domain group) id, or none for domains filed under no client. |
| band | query | "compliant" | "attention" | "risk" | compliant ≥ 90, attention 70–89, risk < 70 — the panel’s own colour bands. |
| hasLeaks | query | "true" | "false" | true: known to have leaks. false: known to be clean. A domain never assessed matches neither — it is not known to be either. |
| unpublished | query | "true" | "false" | |
| sort | query | "client" | "hostname" | "score" | "leaks" | "lastScan" | "consentRate" | |
| dir | query | "asc" | "desc" | |
| group | query | "client" | "none" | Group rows by client. The grouping always runs A–Z; dir orders within it. |
| page | query | number | |
| limit | query | number | Fifty is one screen of the dense table. |
| Status | Body | When |
|---|---|---|
| 200 | PortfolioResponseDto |
| Field | Type | What it is |
|---|---|---|
| items required | PortfolioRowDto[] | |
| items[].domainId required | string | |
| items[].hostname required | string | |
| items[].status required | "pending" | "verified" | "failed" | |
| items[].group required | PortfolioGroupRefDto | null | |
| items[].score required | number | The Compliance screen’s score for the same domain, exactly. |
| items[].band required | "compliant" | "attention" | "risk" | |
| items[].critical required | number | |
| items[].warnings required | number | |
| items[].published required | boolean | |
| items[].publishedAt required | string | null | |
| items[].lastScan required | PortfolioLastScanDto | null | |
| items[].leaks required | PortfolioLeaksDto | null | Null when no scan has ever been assessed — unknown, never zero. |
| items[].consentRate required | number | null | Rolling seven days, banner decisions over impressions — the overview’s Consent rate. Null when nothing was shown. |
| items[].trend required | number[] | One point per UTC day, oldest first. Null for a day with no impressions. |
| total required | number | Rows after filtering, before paging. |
| page required | number | |
| limit required | number | |
| groups required | PortfolioGroupCountDto[] | |
| groups[].id required | string | null | Null for domains under no client. |
| groups[].name required | string | |
| groups[].color required | string | null | |
| groups[].count required | number | Rows in the whole filtered result, not just this page. |
| clients required | PortfolioGroupRefDto[] | Every client this member can see, for the filter — including empty ones. |
| clients[].id required | string | |
| clients[].name required | string | |
| clients[].color required | string | null | |
| facets required | PortfolioFacetsDto | |
| facets.band required | object | Per band, after search and client but before the band filter. |
| facets.withLeaks required | number | |
| facets.unpublished required | number | |
| facets.notAssessed required | number | Never assessed: leaks unknown. |
| summary required | PortfolioSummaryDto | |
| summary.domains required | number | Every domain this member can see, before any filter. |
| summary.withLeaks required | number | |
| summary.unpublished required | number | |
| summary.notAssessed required | number | |
| summary.averageScore required | number | null | |
| trendDays required | number | Points in each trend. |
| rateWindowDays required | number | Window of consentRate, in days. |
/v1/portfolio/bulk/applyDo it, one domain at a time, and report every one
Re-judged rather than trusting the preview, which may be minutes old. A refused domain does not stop the others. Idempotent by batchId: sending the same batch again reports what it already did instead of doing it twice, so Approve is safe to press after a dropped connection.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| action required | "publish" | "scan" | |
| domainIds required | string (uuid)[] | |
| batchId required | string (uuid) | Generated by the caller BEFORE the preview, and sent again with the apply. That is what makes Approve safe to press twice: the second press finds the work already recorded against this batch and reports it rather than doing it again. A server-generated id would have to survive a round trip through the client anyway, and would not cover the case the client cares about — a connection dropped between sending the apply and seeing the answer. |
| Status | Body | When |
|---|---|---|
| 200 | BulkActionResponseDto |
| Field | Type | What it is |
|---|---|---|
| batchId required | string | |
| action required | "scan" | "publish" | |
| preview required | boolean | True for a preview, false for an apply. |
| outcomes required | BulkOutcomeDto[] | |
| outcomes[].domainId required | string | |
| outcomes[].hostname required | string | |
| outcomes[].outcome required | "failed" | "done" | "refused" | "will_change" | "no_change" | "already_done" | |
| outcomes[].reason required | string | null | Why, in words a customer can act on. Null when there is nothing to explain. For a refusal this is the whole value of the preview: "the banner has no Reject all button" is what turns a bulk publish from a gamble into a list of things to fix. |
| outcomes[].code required | string | null | A machine-readable tag for the refusal, so the panel can group or link. |
| summary required | object | |
| summary.total required | number | |
| summary.willChange required | number | |
| summary.noChange required | number | |
| summary.refused required | number | |
| summary.done required | number | |
| summary.alreadyDone required | number | |
| summary.failed required | number |
/v1/portfolio/bulk/previewWhat a bulk action would do, before it does any of it
One row per selected domain: will_change, no_change, or refused with the reason in the words a customer can act on — the dark-pattern gate’s rule, the plan allowance, or a client this member may see but not publish for. Nothing is written. Only domains:list is needed, because nothing changes.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| action required | "publish" | "scan" | |
| domainIds required | string (uuid)[] | |
| batchId required | string (uuid) | Generated by the caller BEFORE the preview, and sent again with the apply. That is what makes Approve safe to press twice: the second press finds the work already recorded against this batch and reports it rather than doing it again. A server-generated id would have to survive a round trip through the client anyway, and would not cover the case the client cares about — a connection dropped between sending the apply and seeing the answer. |
| Status | Body | When |
|---|---|---|
| 200 | BulkActionResponseDto |
| Field | Type | What it is |
|---|---|---|
| batchId required | string | |
| action required | "scan" | "publish" | |
| preview required | boolean | True for a preview, false for an apply. |
| outcomes required | BulkOutcomeDto[] | |
| outcomes[].domainId required | string | |
| outcomes[].hostname required | string | |
| outcomes[].outcome required | "failed" | "done" | "refused" | "will_change" | "no_change" | "already_done" | |
| outcomes[].reason required | string | null | Why, in words a customer can act on. Null when there is nothing to explain. For a refusal this is the whole value of the preview: "the banner has no Reject all button" is what turns a bulk publish from a gamble into a list of things to fix. |
| outcomes[].code required | string | null | A machine-readable tag for the refusal, so the panel can group or link. |
| summary required | object | |
| summary.total required | number | |
| summary.willChange required | number | |
| summary.noChange required | number | |
| summary.refused required | number | |
| summary.done required | number | |
| summary.alreadyDone required | number | |
| summary.failed required | number |
/v1/portfolio/clients/{groupId}/reportOne client's month: score, what changed, every site, what is still wrong
A domain group is a client. The period is a calendar month (YYYY-MM), the current one by default, and it ends at the moment of generation rather than at the end of the month — a report covering a third of a month must not invite comparison with a whole one. Unknowns are null and never zero: a site nothing has been established about has no leak count, and one with no banner impressions has no consent rate. A member scoped to some clients cannot produce a report for another; the database itself enforces this.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| groupId required | path | string | |
| period | query | string | The calendar month, YYYY-MM. Defaults to the current one. A month rather than an arbitrary range because that is the unit an agency bills and reports in, and because two clients' reports for "the last 30 days" generated a day apart cover different periods and cannot be compared. |
| Status | Body | When |
|---|---|---|
| 200 | ClientReportDto |
| Field | Type | What it is |
|---|---|---|
| groupId required | string | |
| groupName required | string | |
| periodStart required | string | Inclusive, ISO. |
| periodEnd required | string | Exclusive, ISO: now for the month in progress. |
| generatedAt required | string | |
| preparedBy required | string | The workspace's branding name, or the workspace's own name. |
| summary required | object | |
| summary.domains required | number | |
| summary.averageScore required | number | null | |
| summary.withLeaks required | number | |
| summary.unpublished required | number | |
| summary.notAssessed required | number | |
| summary.consentRate required | number | null | |
| summary.impressions required | number | |
| summary.decisions required | number | |
| changes required | object | |
| changes.reports required | number | |
| changes.added required | number | |
| changes.fixed required | number | |
| changes.regressed required | number | |
| schedule required | ClientReportScheduleDto | Sent monthly, and to whom — so one request fills the whole screen. |
| schedule.monthly required | boolean | |
| schedule.recipients required | string[] | Who receives the report. Empty means the workspace's owners. |
| domains required | ClientReportDomainDto[] | |
| domains[].hostname required | string | |
| domains[].score required | number | |
| domains[].band required | "compliant" | "attention" | "risk" | |
| domains[].critical required | number | |
| domains[].warnings required | number | |
| domains[].published required | boolean | |
| domains[].leaks required | number | null | Leaks on the newest scan that established anything. Null when none has. |
| domains[].assessedAt required | string | null | When that scan ran. Null when nothing has been assessed. |
| domains[].verdict required | "blocked" | "not_run" | "assessed" | "undetermined" | null | What the newest scan concluded, including "we could not look". |
| domains[].consentRate required | number | null | Rolling seven days. Null with no impressions — never zero. |
| openFindings required | ClientReportFindingDto[] | Everything still wrong, newest report per domain, worst first. |
| openFindings[].hostname required | string | |
| openFindings[].type required | string | |
| openFindings[].severity required | string | |
| openFindings[].title required | string | |
| openFindings[].detail required | string | |
| openFindings[].pageUrl required | string |
/v1/portfolio/clients/{groupId}/report.pdfThe same report as the PDF an agency sends its client
Carries the workspace's branding on Agency and Enterprise. Contains no visitor data, and ends with a page explaining how each number was arrived at — the reader was not in the room.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| groupId required | path | string | |
| period | query | string | The calendar month, YYYY-MM. Defaults to the current one. A month rather than an arbitrary range because that is the unit an agency bills and reports in, and because two clients' reports for "the last 30 days" generated a day apart cover different periods and cannot be compared. |
| Status | Body | When |
|---|---|---|
| 200 | — |
/v1/portfolio/clients/{groupId}/scheduleSend this client's report on the first of every month
At most five addresses, and an empty list means the workspace owners — which is the default an agency that wants to read it before forwarding wants. The report covers the month that just ended, and is sent once per address per month however many times the job runs.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| groupId required | path | string |
| Field | Type | What it is |
|---|---|---|
| monthly required | boolean | |
| recipients | string (email)[] | At most five addresses, because this is the one place the product sends mail to people who never signed up with us. A small, fixed list to a fixed document once a month is not worth abusing; an unbounded one would be. |
| Status | Body | When |
|---|---|---|
| 200 | ClientReportScheduleDto |
| Field | Type | What it is |
|---|---|---|
| monthly required | boolean | |
| recipients required | string[] | Who receives the report. Empty means the workspace's owners. |
/v1/portfolio/viewsYour saved portfolio views
Personal: colleagues’ views are not visible.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | PortfolioViewDto[] |
| Field | Type | What it is |
|---|---|---|
| id required | string | |
| name required | string | |
| query required | PortfolioViewQueryDto | |
| query.q | string | Case-insensitive hostname search. |
| query.groupId | string | A client (domain group) id, or none for domains filed under no client. |
| query.band | "compliant" | "attention" | "risk" | compliant ≥ 90, attention 70–89, risk < 70 — the panel’s own colour bands. |
| query.hasLeaks | "true" | "false" | true: known to have leaks. false: known to be clean. A domain never assessed matches neither — it is not known to be either. |
| query.unpublished | "true" | "false" | |
| query.sort | "client" | "hostname" | "score" | "leaks" | "lastScan" | "consentRate" | |
| query.dir | "asc" | "desc" | |
| query.group | "client" | "none" | Group rows by client. The grouping always runs A–Z; dir orders within it. |
| createdAt required | string | |
| updatedAt required | string |
/v1/portfolio/viewsSave the current filters and ordering as a named view
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| name required | string | |
| query required | PortfolioViewQueryDto | |
| query.q | string | Case-insensitive hostname search. |
| query.groupId | string | A client (domain group) id, or none for domains filed under no client. |
| query.band | "compliant" | "attention" | "risk" | compliant ≥ 90, attention 70–89, risk < 70 — the panel’s own colour bands. |
| query.hasLeaks | "true" | "false" | true: known to have leaks. false: known to be clean. A domain never assessed matches neither — it is not known to be either. |
| query.unpublished | "true" | "false" | |
| query.sort | "client" | "hostname" | "score" | "leaks" | "lastScan" | "consentRate" | |
| query.dir | "asc" | "desc" | |
| query.group | "client" | "none" | Group rows by client. The grouping always runs A–Z; dir orders within it. |
| Status | Body | When |
|---|---|---|
| 201 | PortfolioViewDto |
| Field | Type | What it is |
|---|---|---|
| id required | string | |
| name required | string | |
| query required | PortfolioViewQueryDto | |
| query.q | string | Case-insensitive hostname search. |
| query.groupId | string | A client (domain group) id, or none for domains filed under no client. |
| query.band | "compliant" | "attention" | "risk" | compliant ≥ 90, attention 70–89, risk < 70 — the panel’s own colour bands. |
| query.hasLeaks | "true" | "false" | true: known to have leaks. false: known to be clean. A domain never assessed matches neither — it is not known to be either. |
| query.unpublished | "true" | "false" | |
| query.sort | "client" | "hostname" | "score" | "leaks" | "lastScan" | "consentRate" | |
| query.dir | "asc" | "desc" | |
| query.group | "client" | "none" | Group rows by client. The grouping always runs A–Z; dir orders within it. |
| createdAt required | string | |
| updatedAt required | string |
/v1/portfolio/views/{id}Delete a saved view
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| id required | path | string |
| Status | Body | When |
|---|---|---|
| 204 | — |
/v1/portfolio/views/{id}Rename a saved view, or replace what it holds
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| id required | path | string |
| Field | Type | What it is |
|---|---|---|
| name | string | |
| query | PortfolioViewQueryDto | |
| query.q | string | Case-insensitive hostname search. |
| query.groupId | string | A client (domain group) id, or none for domains filed under no client. |
| query.band | "compliant" | "attention" | "risk" | compliant ≥ 90, attention 70–89, risk < 70 — the panel’s own colour bands. |
| query.hasLeaks | "true" | "false" | true: known to have leaks. false: known to be clean. A domain never assessed matches neither — it is not known to be either. |
| query.unpublished | "true" | "false" | |
| query.sort | "client" | "hostname" | "score" | "leaks" | "lastScan" | "consentRate" | |
| query.dir | "asc" | "desc" | |
| query.group | "client" | "none" | Group rows by client. The grouping always runs A–Z; dir orders within it. |
| Status | Body | When |
|---|---|---|
| 200 | PortfolioViewDto |
| Field | Type | What it is |
|---|---|---|
| id required | string | |
| name required | string | |
| query required | PortfolioViewQueryDto | |
| query.q | string | Case-insensitive hostname search. |
| query.groupId | string | A client (domain group) id, or none for domains filed under no client. |
| query.band | "compliant" | "attention" | "risk" | compliant ≥ 90, attention 70–89, risk < 70 — the panel’s own colour bands. |
| query.hasLeaks | "true" | "false" | true: known to have leaks. false: known to be clean. A domain never assessed matches neither — it is not known to be either. |
| query.unpublished | "true" | "false" | |
| query.sort | "client" | "hostname" | "score" | "leaks" | "lastScan" | "consentRate" | |
| query.dir | "asc" | "desc" | |
| query.group | "client" | "none" | Group rows by client. The grouping always runs A–Z; dir orders within it. |
| createdAt required | string | |
| updatedAt required | string |
Next groupIntegrations