API reference

Portfolio

Every domain in the workspace at once, and actions across many of them.

  1. GET/v1/portfolio
  2. POST/v1/portfolio/bulk/apply
  3. POST/v1/portfolio/bulk/preview
  4. GET/v1/portfolio/clients/{groupId}/report
  5. GET/v1/portfolio/clients/{groupId}/report.pdf
  6. PUT/v1/portfolio/clients/{groupId}/schedule
  7. GET/v1/portfolio/views
  8. POST/v1/portfolio/views
  9. DELETE/v1/portfolio/views/{id}
  10. PUT/v1/portfolio/views/{id}
GET/v1/portfolio

Every 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>.

Parameters
NameWhereTypeWhat it is
qquerystringCase-insensitive hostname search.
groupIdquerystringA client (domain group) id, or none for domains filed under no client.
bandquery"compliant" | "attention" | "risk"compliant ≥ 90, attention 70–89, risk < 70 — the panel’s own colour bands.
hasLeaksquery"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.
unpublishedquery"true" | "false"
sortquery"client" | "hostname" | "score" | "leaks" | "lastScan" | "consentRate"
dirquery"asc" | "desc"
groupquery"client" | "none"Group rows by client. The grouping always runs A–Z; dir orders within it.
pagequerynumber
limitquerynumberFifty is one screen of the dense table.
Responses
StatusBodyWhen
200PortfolioResponseDto
200 body · PortfolioResponseDto
FieldTypeWhat it is
items requiredPortfolioRowDto[]
items[].domainId requiredstring
items[].hostname requiredstring
items[].status required"pending" | "verified" | "failed"
items[].group requiredPortfolioGroupRefDto | null
items[].score requirednumberThe Compliance screen’s score for the same domain, exactly.
items[].band required"compliant" | "attention" | "risk"
items[].critical requirednumber
items[].warnings requirednumber
items[].published requiredboolean
items[].publishedAt requiredstring | null
items[].lastScan requiredPortfolioLastScanDto | null
items[].leaks requiredPortfolioLeaksDto | nullNull when no scan has ever been assessed — unknown, never zero.
items[].consentRate requirednumber | nullRolling seven days, banner decisions over impressions — the overview’s Consent rate. Null when nothing was shown.
items[].trend requirednumber[]One point per UTC day, oldest first. Null for a day with no impressions.
total requirednumberRows after filtering, before paging.
page requirednumber
limit requirednumber
groups requiredPortfolioGroupCountDto[]
groups[].id requiredstring | nullNull for domains under no client.
groups[].name requiredstring
groups[].color requiredstring | null
groups[].count requirednumberRows in the whole filtered result, not just this page.
clients requiredPortfolioGroupRefDto[]Every client this member can see, for the filter — including empty ones.
clients[].id requiredstring
clients[].name requiredstring
clients[].color requiredstring | null
facets requiredPortfolioFacetsDto
facets.band requiredobjectPer band, after search and client but before the band filter.
facets.withLeaks requirednumber
facets.unpublished requirednumber
facets.notAssessed requirednumberNever assessed: leaks unknown.
summary requiredPortfolioSummaryDto
summary.domains requirednumberEvery domain this member can see, before any filter.
summary.withLeaks requirednumber
summary.unpublished requirednumber
summary.notAssessed requirednumber
summary.averageScore requirednumber | null
trendDays requirednumberPoints in each trend.
rateWindowDays requirednumberWindow of consentRate, in days.
POST/v1/portfolio/bulk/apply

Do 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>.

Request body · BulkActionRequestDto
FieldTypeWhat it is
action required"publish" | "scan"
domainIds requiredstring (uuid)[]
batchId requiredstring (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.
Responses
StatusBodyWhen
200BulkActionResponseDto
200 body · BulkActionResponseDto
FieldTypeWhat it is
batchId requiredstring
action required"scan" | "publish"
preview requiredbooleanTrue for a preview, false for an apply.
outcomes requiredBulkOutcomeDto[]
outcomes[].domainId requiredstring
outcomes[].hostname requiredstring
outcomes[].outcome required"failed" | "done" | "refused" | "will_change" | "no_change" | "already_done"
outcomes[].reason requiredstring | nullWhy, 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 requiredstring | nullA machine-readable tag for the refusal, so the panel can group or link.
summary requiredobject
summary.total requirednumber
summary.willChange requirednumber
summary.noChange requirednumber
summary.refused requirednumber
summary.done requirednumber
summary.alreadyDone requirednumber
summary.failed requirednumber
POST/v1/portfolio/bulk/preview

What 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>.

Request body · BulkActionRequestDto
FieldTypeWhat it is
action required"publish" | "scan"
domainIds requiredstring (uuid)[]
batchId requiredstring (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.
Responses
StatusBodyWhen
200BulkActionResponseDto
200 body · BulkActionResponseDto
FieldTypeWhat it is
batchId requiredstring
action required"scan" | "publish"
preview requiredbooleanTrue for a preview, false for an apply.
outcomes requiredBulkOutcomeDto[]
outcomes[].domainId requiredstring
outcomes[].hostname requiredstring
outcomes[].outcome required"failed" | "done" | "refused" | "will_change" | "no_change" | "already_done"
outcomes[].reason requiredstring | nullWhy, 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 requiredstring | nullA machine-readable tag for the refusal, so the panel can group or link.
summary requiredobject
summary.total requirednumber
summary.willChange requirednumber
summary.noChange requirednumber
summary.refused requirednumber
summary.done requirednumber
summary.alreadyDone requirednumber
summary.failed requirednumber
GET/v1/portfolio/clients/{groupId}/report

One 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>.

Parameters
NameWhereTypeWhat it is
groupId requiredpathstring
periodquerystringThe 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.
Responses
StatusBodyWhen
200ClientReportDto
200 body · ClientReportDto
FieldTypeWhat it is
groupId requiredstring
groupName requiredstring
periodStart requiredstringInclusive, ISO.
periodEnd requiredstringExclusive, ISO: now for the month in progress.
generatedAt requiredstring
preparedBy requiredstringThe workspace's branding name, or the workspace's own name.
summary requiredobject
summary.domains requirednumber
summary.averageScore requirednumber | null
summary.withLeaks requirednumber
summary.unpublished requirednumber
summary.notAssessed requirednumber
summary.consentRate requirednumber | null
summary.impressions requirednumber
summary.decisions requirednumber
changes requiredobject
changes.reports requirednumber
changes.added requirednumber
changes.fixed requirednumber
changes.regressed requirednumber
schedule requiredClientReportScheduleDtoSent monthly, and to whom — so one request fills the whole screen.
schedule.monthly requiredboolean
schedule.recipients requiredstring[]Who receives the report. Empty means the workspace's owners.
domains requiredClientReportDomainDto[]
domains[].hostname requiredstring
domains[].score requirednumber
domains[].band required"compliant" | "attention" | "risk"
domains[].critical requirednumber
domains[].warnings requirednumber
domains[].published requiredboolean
domains[].leaks requirednumber | nullLeaks on the newest scan that established anything. Null when none has.
domains[].assessedAt requiredstring | nullWhen that scan ran. Null when nothing has been assessed.
domains[].verdict required"blocked" | "not_run" | "assessed" | "undetermined" | nullWhat the newest scan concluded, including "we could not look".
domains[].consentRate requirednumber | nullRolling seven days. Null with no impressions — never zero.
openFindings requiredClientReportFindingDto[]Everything still wrong, newest report per domain, worst first.
openFindings[].hostname requiredstring
openFindings[].type requiredstring
openFindings[].severity requiredstring
openFindings[].title requiredstring
openFindings[].detail requiredstring
openFindings[].pageUrl requiredstring
GET/v1/portfolio/clients/{groupId}/report.pdf

The 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>.

Parameters
NameWhereTypeWhat it is
groupId requiredpathstring
periodquerystringThe 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.
Responses
StatusBodyWhen
200—
PUT/v1/portfolio/clients/{groupId}/schedule

Send 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>.

Parameters
NameWhereTypeWhat it is
groupId requiredpathstring
Request body · SetClientReportScheduleDto
FieldTypeWhat it is
monthly requiredboolean
recipientsstring (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.
Responses
StatusBodyWhen
200ClientReportScheduleDto
200 body · ClientReportScheduleDto
FieldTypeWhat it is
monthly requiredboolean
recipients requiredstring[]Who receives the report. Empty means the workspace's owners.
GET/v1/portfolio/views

Your saved portfolio views

Personal: colleagues’ views are not visible.

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Responses
StatusBodyWhen
200PortfolioViewDto[]
200 body · PortfolioViewDto[]
FieldTypeWhat it is
id requiredstring
name requiredstring
query requiredPortfolioViewQueryDto
query.qstringCase-insensitive hostname search.
query.groupIdstringA 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 requiredstring
updatedAt requiredstring
POST/v1/portfolio/views

Save the current filters and ordering as a named view

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Request body · CreatePortfolioViewDto
FieldTypeWhat it is
name requiredstring
query requiredPortfolioViewQueryDto
query.qstringCase-insensitive hostname search.
query.groupIdstringA 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.
Responses
StatusBodyWhen
201PortfolioViewDto
201 body · PortfolioViewDto
FieldTypeWhat it is
id requiredstring
name requiredstring
query requiredPortfolioViewQueryDto
query.qstringCase-insensitive hostname search.
query.groupIdstringA 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 requiredstring
updatedAt requiredstring
DELETE/v1/portfolio/views/{id}

Delete a saved view

Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.

Parameters
NameWhereTypeWhat it is
id requiredpathstring
Responses
StatusBodyWhen
204—
PUT/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>.

Parameters
NameWhereTypeWhat it is
id requiredpathstring
Request body · UpdatePortfolioViewDto
FieldTypeWhat it is
namestring
queryPortfolioViewQueryDto
query.qstringCase-insensitive hostname search.
query.groupIdstringA 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.
Responses
StatusBodyWhen
200PortfolioViewDto
200 body · PortfolioViewDto
FieldTypeWhat it is
id requiredstring
name requiredstring
query requiredPortfolioViewQueryDto
query.qstringCase-insensitive hostname search.
query.groupIdstringA 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 requiredstring
updatedAt requiredstring

Next groupIntegrations