Changelog
Every published version of the Page Lookout API contract, newest first. The rules for what changes a version number are in Versioning.
1.2.0 (unreleased)
The date is set when the release ships. Everything below is added: nothing that exists changes meaning, and a client that ignores fields and values it does not know keeps working.
- Your organization's own cookie and storage-key classifications: three more operations, with
the
scans:readscope, read what your organization has said a cookie or a storage key is for.GET .../consent-classificationslists the entries in keyset pages (follownextCursorascursoruntil it isnull): each hasscope(organization, orsitewith itsdomainId),kind(cookieorstorage_key),match(exact, the whole name, orprefix, names that start with it),value, for a cookie the optionalregistrableDomainthat must have set it,category,essential,vendor, a privatenoteand who last changed it and when (createdBy,updatedBy). Narrow it withkind,domainId,scope(not together withdomainId: that is a400) andq;countssays how many entries the caller can see (an organization holds at most 500) and how much of the 65,536 bytes one site's scan can carry the largest site's set uses (siteBytesUsedMax).GET .../consent-classifications/{entryId}reads one entry; an id that names nothing, an entry of another organization and an entry of a site the caller cannot see are all answered404.GET .../domains/{domainId}/consent-classifications/effectivereturns the set the site's next scan would use - the site's own entries and the organization-wide ones, in two lists - with aversionthat is the same for two scans judged with the same set,entryCount,bytesUsedandbytesLimit;blockandversionarenullwhen no entry applies. A site's entry wins over an organization-wide one, and the most specific match wins inside each. IncreatedByandupdatedBy,idanddisplayNamearenullfor an API key (a key is told that a member decided, not who); the whole actor isnullwhen that person is no longer a member, or was Page Lookout staff acting through platform access. A key cannot add, change or remove an entry: those operations are on the session routes, not in this document, and a key that calls them is answered403withdetails.reasonapi_credential_not_allowed. - Entries apply from the next scan: a saved entry changes no finding already raised. Each scan is
judged with the set its site had when the scan started, so an entry applies from each site's next
scan, and the inventory says so until then (below). The first scan after a site's set changes is
compared with the one before it as a transition: consent findings that
appear are
incomparableand those that disappear areunscanned, neverneworresolved, a consent finding present in both is not reported as changed, no regression alert fires for it, and the comparison carries a warning whosedetails.differingKeysincludesorganizationClassification. The scan after that compares as usual. organizationClassificationon the consent inventory: every cookie row of a site's list, every storage-key row and every row of the organization's cookie list carriesorganizationClassification(treat it asnullwhen it is absent), worked out when you ask against the entries as they are now. It isnullwhen no entry applies, and otherwise astate:applied(the scan used this entry),pending(an entry matches that the scan did not use, so it applies from the next scan; shown only for a site's newest scan and only while the feature is on) orremoved(the scan used an entry that has since been removed; it carries nothing else).appliedandpendingcarry the entry that decides:entryId,scope,match,value,registrableDomain,category,essential,vendor,classifiedAtandclassifiedBy(an actor, as above). On the organization's cookie list the field is the answer the row's sites agree on, and{ "state": "differs_by_site", "sites": n }when they do not, which names no entry. The note is never returned. These are your organization's classifications and are never presented as Page Lookout's.classifiedBy: organization: a row'sclassification.classifiedBy(and asourceIdin the summary'sclassificationVersions) can now also beorganization, next tobuiltin,open-cookie-databaseandoperator:<id>; itsclassificationVersionis the 16-characterversionof the set the scan used. Do not assumeclassifiedByis one of a fixed list of values. The same value is inevidence.observed.classifiedByof a consent finding that an entry judged. Such a finding keeps its confirmation (an entry does not turn it into "needs verification"); the mark below says whose answer it rests on.organizationClassificationon findings: every finding of the findings lists (listScanRunFindings,listDomainFindings,listOrganizationFindings) carriesorganizationClassification(treat it asnullwhen it is absent). For a consent finding that an organization entry judged it holds the entry as it was when the scan ran -scope,match,value- and when and by whom it was last changed (classifiedAt,classifiedBy); it isnullfor every other finding, which is not a statement that Page Lookout classified anything.- Five more CSV columns:
cookies.csvof a site,storage-keys.csvof a site andcookies.csvof the organization end withOrganization classification(applied,applies from next scan,removed since this scan,differs by siteon the organization file, or empty),Organization category,Organization essential,Organization classified by(a name; empty for an API key, for someone who is no longer a member, or for Page Lookout staff acting through platform access) andOrganization classified on(a day). The last four describe the entry as it is now. Match columns by their header name, as before. - Report snapshot format:
version.snapshotFormaton the report document read may bereport.snapshot/3, which adds how many items of the scan your organization's entries classified and how many findings rely on them - counts and the set's version, never a name or a person. The report-snapshot route still servesreport.snapshot/1to API keys. - What the dashboard says when an entry is refused: writes are on the session routes, and their
sentences are stable. An organization holds at most 500 entries: "Your organization has 500
classifications, the most it can hold. Remove one to add another." (
422). The entries that apply to one site fit in 65,536 bytes: "Too many classifications apply to {hostname} for one scan to carry. Remove some, or replace several with one 'starts with' entry." (422). The same kind, match, name and cookie domain twice in one scope: "This name already has a classification here. Edit it instead." (409). A name that holds a placeholder the inventory writes for a changing part ({id},{token}or{at}): "Use the name as the site writes it, or 'starts with' for a name that changes." (400). None carries adetails.reason, and no request an API key can make is refused with any of them. - Every site a third party or a cookie appears on: four more operations with the
scans:readscope.GET .../consent-inventory/trackers/sites?registrableDomain=andGET .../consent-inventory/cookies/sites?name=&domain=®istrableDomain=&firstParty=list, in keyset pages (follownextCursorascursoruntil it isnull), the sites whose latest inventory has that row, in hostname order. Each site carriesscanRunId,lastSeenAt,seen,scenarioChecked,pagesCount,newSincePreviousand that site's ownclassification, plus, for a third party,requestCount,owner,approvalandapprovalSource, and, for a cookie,setBy,lifetime,maxLifetimeDaysandorganizationClassification(that site's own answer; neverdiffers_by_site). Identify the row with its own fields from the list, matched exactly.siteIdandtagIdnarrow the sites as on the list, sototalequals the row'ssitesCountunder the same filters. A row that matches nothing is an empty list..../trackers/sites.csvand.../cookies/sites.csvare the same lists as CSV: at most 10,000 rows, the row's identity repeated on each line, and audited. - Findings that need verification, per module: each entry of
byModuleinGET .../dashboard/summarycarriesneedsVerification, shaped likefindingCounts(totalandbySeverity, every severity present, zeroes included): how many of that module's findings need verification, by severity, counted over the same findings asfindingCounts, so no severity is higher than it is there. A server older than 1.2.0 leaves it out, which means the count is not known. - Plain description and next step on issues: each issue of
GET .../domains/{domainId}/issuescarriesdescriptionandrecommendation(from its newest finding, liketitle; either may benull) andneedsVerification(whether that finding needs a person to check it). A server older than 1.2.0 leaves them out. - WCAG criteria on issues: each issue of
GET .../domains/{domainId}/issuescarrieswcagCriteria, the WCAG 2.2 success criteria the check behind it relates to, taken from its newest finding. Each entry hasnumber(1.4.3),name(Contrast (Minimum)),level(A,AAorAAA, the level the standard gives that criterion),since(the first WCAG version that has it:2.0,2.1or2.2),removedIn(2.2for 4.1.1 Parsing, which WCAG 2.2 removed, otherwisenull) andurl(the W3C page that explains the criterion). The entries are in numeric order, and a check can relate to more than one criterion. The list is empty for an issue that is not about accessibility and for a check that names no criterion. A criterion in the list is one the check relates to; it is not a statement about the page. A server older than 1.2.0 leaves the field out. - WCAG criteria on findings, and filters by criterion and level: each finding of the findings
lists (
GET .../scans/{scanRunId}/findings,GET .../domains/{domainId}/findingsandGET .../findings) carries the samewcagCriteria, read from its own evidence (an empty list for a finding that is not about accessibility). All three lists andGET .../domains/{domainId}/issuestake two optional filters.wcagis a WCAG 2.2 criterion number such as1.4.3, andwcagLevelisA,AAorAAA, the level WCAG gives the criterion. Both are repeatable (a repeat is an "or": a finding that cites any of the values matches). With both, the level narrows the criteria: a finding matches when it cites a criterion that is one of the numbers and of one of the levels. A number that is not a criterion of WCAG 2.2, or a level that is not one of the three, is a400; a criterion with no findings is an empty list. A finding whose stored tags were cut down to the rule set (the most heavily trimmed evidence, stored before this version) names no criterion and is not found by either filter. On the issues list the filters narrow the findings before they are grouped, likeseverity, so an issue's counts, pages and first and last seen describe only its matching findings, and itswcagCriteriaare those of the newest matching finding. A server older than 1.2.0 ignores both filters and leaves the field out. - Filter issues by their own state:
GET .../domains/{domainId}/issuestakes an optionalstate(open,resolvedorsuppressed). It narrows the issues, not the findings behind them asstatusdoes:state=openleaves out an issue the latest scan closed (its findings are still stored as open, sostatus=openreturns it). It is applied beforelimitandoffset, andtotalcounts the matching issues. At most 5,000 issues of a site are considered; when the site has more, the response carriesstateLimited: trueanditemsandtotalare a floor. A server older than 1.2.0 ignoresstateand leavesstateLimitedout. - Reserved: the scope name
consent_classifications:writeis held for a later version and is not defined: no key can hold it.
1.1.0 (2026-10-18)
Released with the first production release. This version is frozen: the next change to the contract is published as 1.2.0 (or a patch, for a documentation correction).
- Approved third parties: three more operations, with the
scans:readscope, that read the list of third parties your organization has said it expects on its sites - the registrable domains a consent scan is judged against.GET .../consent-approvalssays whether the list is on (enabled, off until an administrator switches it on in the dashboard), itslistVersionand how many entries it holds.GET .../consent-approvals/trackerslists the entries by domain, in pages: each hasscope(organizationorsite, withdomainId),registrableDomain,effect(allow, orexcludefor a domain expected everywhere except on that site) and anote; narrow it withdomainId,scope=organization,effectandq, and follownextCursorascursoruntil it isnull.scope=organizationtogether witheffect=excludeis refused with a400, since an organization-wide entry is never an exclusion.GET .../domains/{domainId}/consent-approvals/effectivereturns the list one site is judged against - the organization-wide allows plus the site's own, minus the site's exclusions - with aneffectiveHashthat is the same for two scans judged against the same set. A key cannot change the list; that stays with organization administrators in the dashboard. The switch is one-way: once an organization has turned the list on, it stays on. Asking to turn it off (PUT .../consent-approvals/settingswithenabled: false) is answered409withdetails.reasonconsent_approvals_cannot_turn_off, for every role; approve the third parties you expect instead. Page Lookout support can still turn it off for an organization. - Findings for third parties not on the approved list: while an organization has switched its
approved list on, a consent scan raises a finding with
ruleIdconsent.vendors.unapproved_tracker(moduleconsent, no page) for each third party it saw on a site that is not on that site's list. Its severity follows the worst scenario the third party was seen in:highafter Reject all,mediumbefore a consent choice,lowonly after Accept all. These findings appear in every findings list and count in the run's summary like any other; approving the third party and scanning again resolves them. The list cannot be turned off by customers, so a finding is never left behind by the switch. Nothing about existing responses changes while the list is off. - Approved third parties in the consent inventory: every third-party row now says whether the
domain is on your approved list, as it is at the moment you ask. A site's
trackerslist hasapproval(approvedornot_approved) andapprovalSource(organization,site, orexcludedwhen an organization-wide entry covers the domain but the site opted out of it). The organizationtrackerslist hasapproval-approvedonly when the domain is on the list of every site the row appears on - andunapprovedSitesCount, the number of those sites where it is not. While an administrator has the list switched off, these fields arenullrather thannot_approved. The twotrackers.csvfiles gain columns at the end:ApprovalandApproval sourcefor a site,ApprovalandSites not on approved listfor the organization; the cells are empty while the list is off. Clients should ignore fields they do not know. - Open Cookie Database source and third-party owner in the consent inventory: a row's
classification.classifiedBycan now also beopen-cookie-database- the community cookie list the classification may draw on, whoseclassificationVersionis a date and a short commit id, for example2026-09-30+a1b2c3d- next tobuiltinandoperator:<id>; the same value can appear in the summary'sclassificationVersions. A site'strackerslist gainsowner: for a third party that has noclassificationbecause its domain serves several purposes (a search engine's domain that also hosts sign-in and advertising), the company that owns the domain; otherwisenull. A classified row's owner is itsclassification.vendor, soownerisnullthere. The organizationtrackerslist hasownertoo: the owner given by the first site (in hostname order) that has one,nullwhen the row is classified or no owner is known. Bothtrackers.csvfiles gain a last column,Owner: the classification's vendor, else the owner, else empty. Existing responses are unchanged until the environment's classification release switches the new source on. Clients should ignore fields they do not know and should not assumeclassifiedByis one of two values. - Cookie lifetime findings: a consent scan can raise two more rules, both module
consent, severitylow:consent.cookies.long_lifetime_before_choice(a cookie of any category already present before a choice, set to last more than 397 days) andconsent.cookies.long_lifetime_after_reject(the same after Reject all). They appear in findings lists and in run summaries like any other finding; their evidence gives the lifetime in days and where it was read from. The inventory'slifetimebuckets are unchanged (gte_13mstarts at 395 days). A comparison between two scans on either side of the release marks consent findings that appear or disappear asincomparableorunscannedrather thanneworresolved, once. - Approved-list findings name the owner: the description of a
consent.vendors.unapproved_trackerfinding for a domain with anownerreads, for example, "google.com (Unclassified; owned by Google) was contacted on ...", and its evidence hasobserved.owner. The finding's identity is unchanged. - Consent inventory across sites and as CSV: seven more operations, with the
scans:readscope.GET .../consent-inventory/trackersand.../consent-inventory/cookiesunder the organization merge the latest inventory run of each site the key can read, by third party or by cookie, and say on how many sites each was seen (sitesCount,newOnSitesCount,exampleSites);siteId(repeatable) andtagIdnarrow the sites, andsitesCoveredandsitesWithoutInventorysay how many of them have an inventory.sitesWithoutInventoryIdsnames the first 200 of the sites that have none (hostname order; the count stays the whole truth), andscenarioSitesObservedsays, for each ofinitial,reject_allandaccept_all, on how many of the sites with an inventory the latest inventory run saw at least one page in that scenario - a scenario at zero was not checked on any site, which is not the same as nothing showing up in it. Every list also has a.csvvariant -trackers.csv,cookies.csvand, under a site,storage-keys.csv- that answers the same filters and sort as a file: UTF-8 with a byte-order mark, CRLF line ends, a header row of English column names, nolimitoroffset, and at most 10 000 rows. TheX-Rows-Truncatedresponse header istruewhen the list had more rows than the file holds; narrow the filters to get the rest. A text cell that begins with=,+,-,@or a tab is prefixed with an apostrophe so a spreadsheet does not run it. Each download of at least one row is recorded against the organization. - Consent signals of a scan run: two more operations, with the
scans:readscope, that read what the pages of one scan run declared about the visitor's consent choice.GET .../scans/{scanRunId}/consent-signalsreturnsrows- one per page, in the order of the page's path, each withpageId,normalizedUrlandscenarios(initial,reject_all,accept_all) - asummaryandnextCursor. A scenario has astatus(observedornot_performed, with areason), the decoded IAB TCF consent string the page returned undertcf(whether the interface answered, whether it applies to the visitor, the consent tool's id, the purposes and special features consented to or declared as legitimate interest, vendor counts, and a digest that tells whether the string changed between scenarios) and what the page's Google tags sent underconsentMode(googleRequestsand, forad_storage,analytics_storage,ad_user_dataandad_personalization, thedefault,updateandsentstate). The consent string itself and the URLs and details of the Google requests are never stored or returned. Thesummarycounts pages of the whole run -pagesRead,pagesWithTcfString,pagesTcfPurposesBeforeChoice,pagesTcfPurposesAfterReject,pagesWithGoogleRequests,pagesConsentModeGrantedBeforeChoiceandpagesConsentModeGrantedAfterReject- and isnullwhen the run has no consent signals (an old run, a run scanned without them, or a run without the consent module), so a screen can say "not read in this scan". Page through the rows withlimit(1-100, default 25) andcursor; a cursor the endpoint did not hand out is a400. A run of another organization and a run that does not exist both answer404, so the endpoint cannot be used to find out which ids exist; a run of your own with nothing to show answers200.GET .../scans/{scanRunId}/consent-signals.csvis the same rows as a file, one line per page and scenario, in the column orderPage path,Scenario,TCF platform id,Applies to visitor,String,Purposes granted,Special features,Vendors with consent,Vendors with legitimate interest, the fourConsent Modestates andGoogle requests: UTF-8 with a byte-order mark, CRLF line ends, at most 10 000 rows withX-Rows-Truncatedas in the consent inventory exports, and the same apostrophe guard on text cells. Each download of at least one row is recorded against the organization. New signals add columns at the end. Clients should ignore fields they do not know. - Consent inventory: four operations read what the consent scans found on a site, with the
scans:readscope:GET .../domains/{domainId}/consent-inventory(the summary of one scan run and up to 20 runs to pick from), and.../consent-inventory/trackers,/cookiesand/storage-keys(a list of each, withscenario,category,classified,new,q,limit,offsetandsortfilters, andpartyorareawhere they apply).scanRunIdpicks an older run. A site with no inventory yet answers200withinventorynull(the summary) orscanRunIdnulland no items (a list).newSincePrevious,newCounts,goneCountsandsamePageSetarenullwhen the run has no previous inventory run to compare with.samePageSetcan be approximate once the previous run's page data has expired. Rows carry names only, never a cookie or storage value, a URL or a path; pages are named by id. - Report snapshot formats:
version.snapshotFormaton the report document read may bereport.snapshot/2; the report-snapshot route still servesreport.snapshot/1to API keys. - Report snapshot alerts: an entry of
alerts.itemsmay havekindhealth_change(a rule that fires when a site's Health turns to Needs attention). It carries the rule's name and the number of results that turned, like every other kind - never a Health number. - Sites: each site of the site list and the site read has
tags- the labels the organization put on it, each withid,nameandcolor(the colour's key, one ofneutral,red,orange,amber,green,teal,blue,violet; never a hex value). They are ordered by name, ignoring case, andtagsis[]- never absent - for a site with none. An organization holds at most 500 tags. A tag is read-only here: the operations that create, rename, delete and put tags on sites are on the session routes, not in this document, and a key cannot call them. - Modules:
modulevalues includegeo(AI search). Until it is available for an organization, a run'smoduleOutcomeslistsgeowith zero tasks, and no finding or issue names it. Naminggeoin an alert rule or a site's policy is on the session routes, which are not in this document. Dashboard summary:GET .../dashboard/summaryhas ageoentry inbyModule, withscannedfalse and zero counts until a scan runs it. Evidence: an evidence value may havekindgeo, with the same fields asseo. - AI search availability:
geois available for an organization only when Page Lookout has switched the module on for the deployment and for that organization. Starting a scan of a site whose policy names onlygeois refused with422, and themessagesays "...names only AI search, which is not switched on for this organization" when that is the reason. It has nodetails.reason. The organization limits read has a booleangeoEnabledbesidelimits-falseuntil the module is switched on for the organization; it is on the session routes and is not in this document. Switching it off removes nothing: stored findings stay readable, and later runs leavegeoout until the module is available again. - Errors:
details.reasonmay bealert_kind_unavailable. No request an API key can make is refused with it. - Errors:
details.reasonmay besite_tag_limit_reached(an organization at its 500 tags) oraccess_group_limit_reached(an organization at its limit of access groups). No request an API key can make is refused with either: they answer the session routes, which are not in this document. - Errors: a separate
409for a tag name that is already taken may carrydetails.existingTagId, the tag that holds the name. It has nodetails.reason, only the session routes answer it, and a key never receives it. - Errors: the closed list of
details.reasonvalues in theErrorResponseschema also names other reasons that only the session routes answer. No request an API key can make is refused with any of them. - Performance runs on a fixed list of pages: a run's performance tasks are now one per page of the site's list of performance pages, not one per page the scan finds. By default the list is the home page and the pages closest to it, up to 20, chosen at the first scan of the site that includes performance and kept after that: every scan measures the same pages. A person with the administrator role can add and remove pages on the site's configuration screen; the organization's limit on the number of pages per site is 20 unless Page Lookout support sets another number. The list is edited on session routes, which are not in this document.
- Fewer performance tasks per run: a run's
moduleOutcomesentry forperformancecounts the tasks of the pages on the list, so on a site with more pages than the list it shows fewer tasks than before. - Performance findings on pages that are not on the list: when a page leaves the list, or was
never on it, its earlier performance findings are not looked at again. A comparison between two
scans reports them once as
unscannedrather thanresolved, and the scan after that has nothing to compare. - Errors:
details.limitmay namemaxPerformancePagesPerSite, the number of performance pages one site may have. No request an API key can make is refused with it.
1.0.0
The first published version of the contract.
- API keys: 22 operations to list sites, start scans and read scan runs, findings, issues and reports, each with the scope it needs (Getting started).
- Errors: one error body for every failure, with
correlationIdand a closed list ofdetails.reasonvalues (Errors and limits). - Rate limits: per key per minute, with
RateLimit-*headers andRetry-After. - Paused sites: a site with
isActivefalseis paused by Page Lookout, and starting a scan of it answers422withdetails.reasonsite_paused. - Raw results: each entry of a run's
moduleOutcomeshasrawResultsMissing- that module's checks that completed but whose raw result could not be saved. Their findings are complete. - Modules:
modulevalues arediscovery,security_baseline,performance,seo,accessibility,consentandvisual. Visual comparison is not available yet: until it is, a run'smoduleOutcomeslistsvisualwith zero tasks, and no finding, issue or alert names it. A run'spolicySnapshotmay carryvisualConfig- the visual comparison settings that run used - and itsappliedOrganizationLimitsmay namevisualScreenshotsPerScan,visualScreenshotsPerMonth,visualViewportsandvisualMaxPageHeight. Neither appears until visual comparison is available. For the same reason anorganization_limit_exceedederror'sdetails.limitmay namemaxVisualScreenshotsPerScan,maxVisualScreenshotsPerMonth,maxVisualViewportsormaxVisualPageHeight; no request an API key can make is refused with them. - Webhooks:
scan.completedandwebhook.ping, payloadapiVersion1, signed with HMAC-SHA256 (Webhooks). - OpenAPI document:
GET /openapi.jsonon the API base URL serves the OpenAPI 3.1 document of this contract - the same one the API reference renders - with no key needed.