← Privacy Ledger
API
Tokens

Run Privacy Ledger from your own code

What your site collects, and what your notice has to say about it. Privacy Ledger takes a website's own HTML and a short business facts sheet and builds the data inventory a privacy notice needs, the way the source skill says a notice is drafted: from verified facts - the jurisdictions, the data actually collected, the purposes, the recipients, the transfers, the retention, the rights contacts, children and cookies - with every unknown kept explicit instead of invented. The model never invents a company name, an email, an address, a retention period, a safeguard or a tool: a fact nobody has given comes back as a placeholder in double square brackets, such as [[how long enquiries are kept]]. The output is a template for informational purposes, not legal advice; it is drafted for review by a qualified attorney and never certifies compliance.

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page builds the inventory for free - it recognises about forty common tools from the tags they load (with what each collects, whether it sets non-essential cookies, sends data to the US or advertises), reads the form fields for personal and sensitive data, works out which laws' disclosures apply from where the visitors are, lists the required disclosures and the unknown facts, checks a pasted notice for each disclosure, each tool and each statement the site contradicts, and runs the PL- findings. That is plscan.js. The run either drafts a reviewable notice from the inventory ("draft") or reviews the notice the business has now against it ("review"); the page then holds the reply to the inventory (plreply.js): every required disclosure present, every tool on the site named and no other, every unknown a placeholder, no retention period or email the facts do not give, no claim of compliance, the disclaimer, and every error and warning answered.

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"privacy-ledger"}), so no slug header is needed afterwards. Send it as Authorization: Bearer .... The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a field the run needs (task, business, site, today, page) is missing or not a string. Send every field as a string; policy is "" for "draft".
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes - "draft" (a reviewable notice from the ledger: a two-sentence summary, the notice as headed sections naming every tool and covering every required disclosure, one unknowns entry per placeholder, and the implementation steps a notice cannot do by itself, such as a cookie banner that blocks the tools until the visitor consents) and "review" (the notice the business has now, sent as policy, checked against the ledger: a verdict of sound, gaps or rework, the statements the site contradicts with a fix each, one gap per missing disclosure with a suggested clause, the tools it does not name and what it already does well). Both lanes send the same fields; policy is "" for "draft". A task the model does not know is answered as draft, with a caveat saying so.

The input

Every field is a string.

FieldRequiredWhat
taskyes"draft" or "review". The model answers that task and only that task.
businessyesthe business facts sheet as written, one fact per line: Business:, Contact:, Address:, Website:, What we do:, Users in:, Children:, Newsletter:, Accounts:, Payments:, Tools: (services the HTML does not show), Retention:, DPO:. A line that is missing is a fact nobody has told the model. Users in: (UK, EU, US (California), Canada, Brazil...) is needed - it decides which laws apply - and the page sends at most 30,000 characters.
siteyesthe evidence lines the page read from the site's HTML, built by plscan.js (PlScan.siteLines(scan)) - the tags that load a known tool, the forms, and the form fields - not the whole HTML. See the site format.
policyreview onlythe text of the privacy notice the business has now (the page wants at least 60 words and at most 30,000 characters). Send "" for "draft".
todayyestoday's date, YYYY-MM-DD. The page uses it to find a notice that is over a year old.
pageyesthe page's inventory of the same facts and HTML, built in the browser by plscan.js (PlScan.pageFacts(scan, task), via buildInput) - see the page format.
retry_notenopresent only on a reformat retry: the previous reply could not be read as one JSON object, and this says how to answer again. Leave it out otherwise.

The site format

site is what a paid run sends instead of the HTML: one line per tag that loads a tool from the catalogue, and per form - the tag name and up to 140 characters around what matched (at most 60 tags) - then one line per kind of form field, with what the field's name, type, label or placeholder said. With no HTML it is (no HTML pasted).

<script|iframe|link|img|form|noscript> <the part of the src, href, action or script body that shows the tool>
field <kind> (<what the name, type, label or placeholder said>)

The lines the page sends for its "clinic's booking site" sample (every business in the samples is invented):

script https://assets.calendly.com/assets/external/widget.js
script https://js.stripe.com/v3/
script https://www.google.com/recaptcha/api.js
iframe https://www.youtube.com/embed/4kT9xYq2Lm0
form /intake
field name (text full name name Full name)
field email (email email Email)
field phone (tel phone Phone)
field date of birth (date dob Date of birth)
field health information (textarea medical history What brings you in? Injuries, condi)

The page format

page is the page's own inventory, written as plain text. The page builds it in the browser with plscan.js (PlScan.buildInput(state, task) runs PlScan.scan(state) and calls PlScan.pageFacts(scan, task)), and the model may use only the facts it prints: every tool it names is one of these, every required disclosure is covered, every unknown fact is a placeholder, every error and warn rule id it lists is answered, and the page then holds the reply to the same inventory. So an API client should send page built the same way - from plscan.js run under node (Build the input yourself; the same facts, HTML, notice and today give the same page as the browser), or the text the page builds for the same site - not an empty string and not an inventory of your own.

Today: <YYYY-MM-DD>.
Business facts: <key> = <value>; ... .                                   (or: none given)
Laws whose disclosures apply: <law>; ... .
Tools (name | category | what it collects | non-essential cookies | US transfer | advertising | found):
- <name> | <category> | <what it collects> | <yes|no|unknown> | <yes|no|unknown> | <yes|no|unknown> | <the tag, or: named under Tools:>
Form fields: <kind>[ (sensitive)], ... .                                  (or: none found)
Features: newsletter <yes|no>; accounts <yes|no>; payments <yes|no>; children <yes|no|unknown>.
Disclosures required (id | law | what):
- <id> | <the laws that apply to this site> | <what>
Unknown facts (leave a [[placeholder]], never invent): <fact> - <what>; ... .   (or: none)
The current notice: <n> words, last updated <YYYY-MM-DD>.                (review only; or: no update date found)
Disclosures in it (id | found):                                           (review only)
- <id> | <found|MISSING>
Tools it names: <name>, ... .                                             (review only; or: none)
Statements the inventory contradicts:                                     (review only, when there are any)
- "<quote from the notice>" - <why>
Findings (rule | severity | where | text):
- <rule id> | <error|warn|info> | <where|-> | <title>[ (note)]

An empty tools or findings list gets the single line - none found or - none. A tool named under Tools: that is not in the catalogue is listed as other (named under Tools:), "not known to the page", with unknown flags. A real one - the page the browser sends for the clinic sample on the "draft" lane:

Today: 2026-10-30.
Business facts: business = Brackwen Physio & Movement LLC; contact = privacy@brackwenphysio.example; address = 418 Velloran Street, Suite 3, Sacramento, CA 95814, USA; website = https://brackwenphysio.example; purpose = physical therapy and movement classes, booked online; users = US (California), Canada; children = no, patients 18 and over; newsletter = no; accounts = no; payments = yes, deposits by card; tools = Google Workspace; dpo = none.
Laws whose disclosures apply: CCPA / CPRA (if over its thresholds); PIPEDA.
Tools (name | category | what it collects | non-essential cookies | US transfer | advertising | found):
- Stripe | payments | payment card details, billing address, fraud signals | no | yes | no | script https://js.stripe.com/v3/
- Google reCAPTCHA | security | device and interaction data, IP address | no | yes | no | script https://www.google.com/recaptcha/api.js
- YouTube embeds | embedded video | viewing data, cookies, IP address | yes | yes | yes | iframe https://www.youtube.com/embed/4kT9xYq2Lm0
- Calendly | scheduling | name, email, booking details | yes | yes | no | script https://assets.calendly.com/assets/external/widget.js
- Google Workspace | other (named under Tools:) | not known to the page | unknown | unknown | unknown | named under Tools:
Form fields: name, email, phone, date of birth, health information (sensitive).
Features: newsletter no; accounts no; payments yes; children no.
Disclosures required (id | law | what):
- purposes | CPRA, PIPEDA | The purposes each kind of data is used for
- recipients | CPRA | Who receives the data (providers and categories of recipients)
- retention | CPRA | How long each kind of data is kept, or how that is decided
- rights | PIPEDA | The person's rights: access and correction everywhere; under the GDPR also erasure, restriction, portability and objection
- complaint | PIPEDA | The right to complain to the regulator for the visitor's country
- cookies | CPRA (California) | Which cookies and similar trackers are used, why, and how to refuse them
- ccpa-categories | CCPA / CPRA | The categories of personal information collected and their sources
- ccpa-rights | CCPA / CPRA | California rights: to know, delete, correct, and not be discriminated against
- ccpa-optout | CCPA / CPRA | The right to opt out of sale or sharing, the link, and Global Privacy Control
- ccpa-requests | CCPA / CPRA | How to make a request (at least two ways, such as a form and an email)
- updated | All | When the notice was last updated
Unknown facts (leave a [[placeholder]], never invent): retention - how long each kind of data is kept.
Findings (rule | severity | where | text):
- PL-SHARE | warn | Sharing | Advertising tags with California visitors (YouTube embeds)
- PL-SENSITIVE | warn | Forms | A form asks for sensitive data (health information)
- PL-UNKNOWN | warn | retention | A fact every notice needs is not known yet (how long each kind of data is kept)
- PL-THRESHOLD | info | California | The CCPA applies only above its thresholds
- PL-PAYMENT | info | Stripe | Payments go through a processor

The laws and the required disclosures

The laws come from Users in: ("worldwide", "global" or "anywhere" means all of them) and the features: the GDPR and the UK GDPR for EU or UK visitors; the ePrivacy Directive and PECR cookie rules when a tool sets non-essential cookies; the CCPA / CPRA for California (if over its thresholds); COPPA when children use the service and visitors are in the US; CAN-SPAM for a newsletter with US visitors and CASL with Canadian ones; PIPEDA for Canada; the LGPD for Brazil. The required disclosures (REQS in plscan.js) are listed with the law that applies to the site; the same keyword test decides whether a notice - the one you paste or the draft - gives each one.

IdLawWhat the notice must sayRequired when
controllerGDPR / UK GDPRWho is responsible for the data (the controller) and how to contact themthe GDPR applies (EU or UK visitors)
dpoGDPR / UK GDPRThe data protection officer's contact detailsthe GDPR applies and a DPO is given
purposesGDPR / UK GDPR, CPRA, PIPEDAThe purposes each kind of data is used foralways
basesGDPR / UK GDPRThe lawful basis for each purpose (consent, contract, legal obligation, legitimate interests)the GDPR applies
recipientsGDPR / UK GDPR, CPRAWho receives the data (providers and categories of recipients)the GDPR applies, or California visitors
transfersGDPR / UK GDPRTransfers outside the UK or EEA and the safeguard usedthe GDPR applies and a tool sends data to the US
retentionGDPR / UK GDPR, CPRAHow long each kind of data is kept, or how that is decidedthe GDPR applies, or California visitors
rightsGDPR / UK GDPR, PIPEDAThe person's rights: access and correction everywhere; under the GDPR also erasure, restriction, portability and objectionthe GDPR applies, or Canadian visitors
withdrawGDPR / UK GDPRThat consent can be withdrawn at any timethe GDPR applies and consent is used (a newsletter or a tool that sets non-essential cookies)
complaintGDPR / UK GDPR, PIPEDAThe right to complain to the regulator for the visitor's countrythe GDPR applies, or Canadian visitors
cookiesePrivacy / PECR (EU and UK); CPRA (California)Which cookies and similar trackers are used, why, and how to refuse thema tool sets non-essential cookies
ccpa-categoriesCCPA / CPRAThe categories of personal information collected and their sourcesCalifornia visitors
ccpa-rightsCCPA / CPRACalifornia rights: to know, delete, correct, and not be discriminated againstCalifornia visitors
ccpa-optoutCCPA / CPRAThe right to opt out of sale or sharing, the link, and Global Privacy ControlCalifornia visitors and an advertising tool
ccpa-requestsCCPA / CPRAHow to make a request (at least two ways, such as a form and an email)California visitors
childrenCOPPA / GDPR Art 8Whether children use the service and how their data is handledChildren: is not no (yes, or not given)
emailCAN-SPAM / CASLMarketing email: how people sign up and unsubscribea newsletter (Newsletter: yes, or an email marketing tool)
lgpdLGPD (Brazil)LGPD rights and the data protection officer (encarregado)Brazilian visitors
updatedAllWhen the notice was last updatedalways

The page's findings

Rule ids the page uses, with their severity and area (RULES in plscan.js). PL-STALE, PL-UNLISTED, PL-MISSING and PL-CONTRA come only with a pasted notice.

RuleSeverityAreaTitle
PL-CONSENTerrorcookiesNon-essential cookies or trackers with EU or UK visitors
PL-CHILDerrorchildrenChildren may use the service
PL-SHAREwarnsharingAdvertising tags with California visitors
PL-TRANSFERwarntransfersData goes to US providers from EU or UK visitors
PL-SENSITIVEwarndataA form asks for sensitive data
PL-EMAILwarnemailEmail marketing without a postal address
PL-UNKNOWNwarnfactsA fact every notice needs is not known yet
PL-THRESHOLDinfosharingThe CCPA applies only above its thresholds
PL-PAYMENTinfodataPayments go through a processor
PL-FONTSinfotransfersFonts loaded from a third party
PL-NOTOOLSinfofactsNo tools found in the HTML
PL-STALEwarnnoticeThe notice is over a year old
PL-UNLISTEDwarnnoticeA tool on the site is not in the notice
PL-MISSINGwarnnoticeA required disclosure is not in the notice
PL-CONTRAerrornoticeThe notice says something the site contradicts

A point the page cannot see (a health-privacy law for a clinic, a contact email that differs from the facts) comes back as a finding with the rule NEW.

The tool catalogue

The tools the page recognises from the tags they load (VENDORS in plscan.js), with what each collects and the three flags the inventory carries. A tool named under Tools: is matched against the same names.

ToolCategoryCollectsNon-essential cookiesUS transferAdvertising
Google Analyticsanalyticsdevice and usage data, IP address, cookie identifiersyesyesno
Google Tag Managertag managerloads other tags; IP addressnoyesno
Google Adsadvertisingadvertising identifiers, conversions, IP addressyesyesyes
Meta Pixeladvertisingpage views, events, advertising identifiers, IP addressyesyesyes
TikTok Pixeladvertisingevents, advertising identifiers, IP addressyesyesyes
LinkedIn Insight Tagadvertisingpage views, professional demographics, IP addressyesyesyes
X (Twitter) Pixeladvertisingevents, advertising identifiersyesyesyes
Pinterest Tagadvertisingevents, advertising identifiersyesyesyes
Hotjaranalyticssession recordings, clicks, device datayesnono
Microsoft Clarityanalyticssession recordings, clicks, device datayesyesno
Plausible Analyticsanalyticsaggregate page views, no cookiesnonono
Fathom Analyticsanalyticsaggregate page views, no cookiesnonono
Segmentanalyticsevents, identifiers, whatever it forwardsyesyesno
Mixpanelanalyticsevents, identifiers, device datayesyesno
Amplitudeanalyticsevents, identifiers, device datayesyesno
PostHoganalyticsevents, session recordings, identifiersyesyesno
Intercomsupport chatname, email, chat messages, usageyesyesno
Crispsupport chatname, email, chat messagesyesnono
Zendesksupport chatname, email, support messagesyesyesno
HubSpotmarketingcontact details, form submissions, page viewsyesyesno
Mailchimpemail marketingemail address, name, email engagementnoyesno
Kit (ConvertKit)email marketingemail address, name, email engagementnoyesno
Klaviyoemail marketingemail address, phone, purchase history, engagementyesyesno
Stripepaymentspayment card details, billing address, fraud signalsnoyesno
PayPalpaymentspayment details, PayPal accountnoyesno
Shopifye-commerceorders, addresses, payment detailsyesnono
Google reCAPTCHAsecuritydevice and interaction data, IP addressnoyesno
hCaptchasecuritydevice and interaction data, IP addressnoyesno
Cloudflare Turnstilesecuritydevice signals, IP addressnoyesno
Cloudflarehosting and securityIP address, request datanoyesno
YouTube embedsembedded videoviewing data, cookies, IP addressyesyesyes
Vimeo embedsembedded videoviewing data, IP addressyesyesno
Google Mapsembedded maplocation searched, IP addressyesyesno
Google FontsfontsIP address, browser detailsnoyesno
Adobe FontsfontsIP address, browser detailsnoyesno
Sentryerror monitoringerror reports, device data, IP addressnoyesno
Calendlyschedulingname, email, booking detailsyesyesno
Typeformformswhatever the form asksyesnono
Disquscommentscomments, profile, trackingyesyesyes
Gravataravatarshashed email addressnoyesno
Vercel Analyticsanalyticsaggregate page viewsnoyesno

The standard the model applies

The source skill - @sickn33/legal-advisor (sickn33/antigravity-awesome-skills, MIT) - is the only standard:

House rules

The replies

Both lanes carry task, summary, findings, prescan and caveats; "draft" adds sections, unknowns and implementation, and "review" adds verdict, contradictions, gaps, tools and keep. The shapes below are what the page reads after PlReply.normalize(): a missing list comes back empty, every value becomes a trimmed string, a finding with no rule becomes NEW and an unknown severity becomes medium, a call other than disagree becomes agree, a verdict other than sound, gaps or rework becomes "", and entries with nothing in them are dropped (a section with no heading and no body, a contradiction with no quote, a tool with no name). A draft is usable when it has at least three sections; a review when it has a summary and a gap or a contradiction (or the verdict sound). The page also refuses a reply that answers the other task.

draft

A reviewable notice, written from the facts and the inventory.

  1. summary - two sentences: what the notice covers (which laws, which tools) and what must be filled in before it is published.
  2. sections - the notice itself, each heading and body (plain text, short paragraphs, lists as lines starting "- "): who we are; what we collect and where it comes from; how and why we use it (with the lawful basis for each purpose when the GDPR applies); who we share it with, naming each tool; cookies and how to refuse them; international transfers; how long we keep it; your rights and the regulator to complain to; children; marketing email; California, when the page lists it; changes and when it was last updated; how to contact us - merging or skipping only what does not apply.
  3. unknowns - one per placeholder used: fact, placeholder (exactly as written in the text) and why (what to find out).
  4. implementation - what the notice cannot do by itself, each item and why: a cookie banner that blocks the tools until consent, the "Do Not Sell or Share" link with Global Privacy Control, a way to handle access and deletion requests, data processing terms with each provider, a postal address in marketing emails.
  5. findings (points the page cannot see: rule NEW, severity high / medium / low, title, where, why, fix); prescan (rule, call, note); caveats (strings).
{
  "task": "draft",
  "summary": "",
  "sections": [{"heading": "", "body": ""}],
  "unknowns": [{"fact": "", "placeholder": "[[...]]", "why": ""}],
  "implementation": [{"item": "", "why": ""}],
  "findings": [{"rule": "NEW", "severity": "medium", "title": "", "where": "", "why": "", "fix": ""}],
  "prescan": [{"rule": "", "call": "agree", "note": ""}],
  "caveats": [""]
}

review

The notice the business has now, against the ledger.

  1. verdict - rework when the notice says something the site contradicts or the page found an error; gaps when disclosures or tools are missing; sound only when the page found neither.
  2. summary - two or three sentences: the verdict, the first thing to correct, and how far the notice is from the inventory.
  3. contradictions - each statement the page lists as contradicted: quote (copied exactly from the notice; the page's quoted fragment is enough), why and fix.
  4. gaps - one per missing disclosure the page lists: requirement (the page's id), law, fix and clause (a suggested replacement paragraph, with placeholders for unknown facts).
  5. tools - each tool the notice does not name, tool (the page's name) and fix; keep - what the notice already does well, a line each.
  6. findings, prescan, caveats - as for "draft".
{
  "task": "review",
  "verdict": "gaps",
  "summary": "",
  "contradictions": [{"quote": "", "why": "", "fix": ""}],
  "gaps": [{"requirement": "", "law": "", "fix": "", "clause": ""}],
  "tools": [{"tool": "", "fix": ""}],
  "keep": [""],
  "findings": [],
  "prescan": [{"rule": "", "call": "agree", "note": ""}],
  "caveats": [""]
}

The page holds either reply to the inventory with PlReply.check() and shows each miss next to the reply as a note. For both lanes it wants no claim of compliance or certification; the disclaimer (a qualified attorney, not legal advice, or for informational purposes); no email address that the facts or the notice do not give; every error and warn rule id answered in prescan or findings; and no rule id the page did not report other than NEW. For "draft" it also wants every required disclosure found in the sections by the same keyword test it runs on a pasted notice; every tool on the site named (fonts and hosting may stay a category) and no catalogue tool the site does not use; a placeholder for every unknown fact; no retention period the facts do not give outside a placeholder; an implementation note on consent when PL-CONSENT was found, and an opt-out or Global Privacy Control when PL-SHARE was; and at least five sections. For "review" it wants every quote found in the notice; every contradicted statement corrected; every missing disclosure listed in gaps; every tool the notice does not name mentioned; the verdict the page's findings call for (rework with a contradiction or an error, gaps with a missing disclosure, an unnamed tool or a warning, sound otherwise); and no retention period in a suggested clause that the facts do not give.

Reading the reply

The job's output.output is text that should be exactly one JSON object. Parse it; if a model ever wraps it in prose or a fence, take the text from the first { to the last } and parse that (the page does the same in plreply.js, and closes a stream that stopped mid-object). If it still does not parse, send the same input again with a retry_note such as "Your previous reply was not usable. Reply again with ONLY the JSON object for task 'draft' - no prose, no code fences." and a new attempt number in the key (see step 5). Step 5 prints, for "draft", the summary, every section with its heading, the placeholders to fill in and what the notice cannot do by itself; for "review", the verdict and summary, the contradictions, the gaps, the tools to name and what to keep; then for both lanes the page's calls, the further points and the caveats.

Draft, then review

The two lanes chain. Once a "draft" has been filled in (every [[placeholder]] replaced) and published, its text becomes the policy of a "review" with the same business and the current HTML, after the next change to the site: the review then catches a tool added since, a statement the site now contradicts, or a notice over a year old. A review needs a page built for the review lane, which adds the notice's own disclosures, the tools it names and the statements the inventory contradicts.

Worked samples

Two of the page's own samples, with the real saved replies it shows for them (the inputs are byte for byte what the page sends). Every business, person, address and notice is invented.

Sample 1 - draft: a clinic's booking site

A physical therapy practice in California with Canadian patients: Calendly, Stripe, reCAPTCHA and a YouTube embed in the HTML, Google Workspace under Tools:, and an intake form that asks for a date of birth and medical history. No retention period is given. The page applies the CCPA / CPRA and PIPEDA, and reports three warnings (PL-SHARE for the YouTube embed, PL-SENSITIVE for the health field, PL-UNKNOWN for retention) and two info findings. Input - business and site are exactly what the page sends; page is shortened here (each ... stands for lines left out - send the whole text the page builds, shown in full under the page format):

{
 "task": "draft",
 "business": "Business: Brackwen Physio & Movement LLC\nContact: privacy@brackwenphysio.example\nAddress: 418 Velloran Street, Suite 3, Sacramento, CA 95814, USA\nWebsite: https://brackwenphysio.example\nWhat we do: physical therapy and movement classes, booked online\nUsers in: US (California), Canada\nChildren: no, patients 18 and over\nNewsletter: no\nAccounts: no\nPayments: yes, deposits by card\nTools: Google Workspace\nDPO: none",
 "site": "script https://assets.calendly.com/assets/external/widget.js\nscript https://js.stripe.com/v3/\nscript https://www.google.com/recaptcha/api.js\niframe https://www.youtube.com/embed/4kT9xYq2Lm0\nform /intake\nfield name (text full name name Full name)\nfield email (email email Email)\nfield phone (tel phone Phone)\nfield date of birth (date dob Date of birth)\nfield health information (textarea medical history What brings you in? Injuries, condi)",
 "policy": "",
 "today": "2026-10-30",
 "page": "Today: 2026-10-30.\n...\nLaws whose disclosures apply: CCPA / CPRA (if over its thresholds); PIPEDA.\nTools (name | category | what it collects | non-essential cookies | US transfer | advertising | found):\n- Stripe | payments | payment card details, billing address, fraud signals | no | yes | no | script https://js.stripe.com/v3/\n...\nForm fields: name, email, phone, date of birth, health information (sensitive).\nFeatures: newsletter no; accounts no; payments yes; children no.\nDisclosures required (id | law | what):\n- purposes | CPRA, PIPEDA | The purposes each kind of data is used for\n...\nUnknown facts (leave a [[placeholder]], never invent): retention - how long each kind of data is kept.\nFindings (rule | severity | where | text):\n- PL-SHARE | warn | Sharing | Advertising tags with California visitors (YouTube embeds)\n- PL-SENSITIVE | warn | Forms | A form asks for sensitive data (health information)\n- PL-UNKNOWN | warn | retention | A fact every notice needs is not known yet (how long each kind of data is kept)\n- PL-THRESHOLD | info | California | The CCPA applies only above its thresholds\n- PL-PAYMENT | info | Stripe | Payments go through a processor"
}

Reply - the saved reply, abbreviated (each /* ... */ comment stands for entries left out, so the block is not valid JSON as shown):

{
 "task": "draft",
 "summary": "This draft notice covers the CCPA/CPRA (if its thresholds are met) and PIPEDA for Brackwen Physio & Movement LLC, naming Calendly, Stripe, Google reCAPTCHA, YouTube embeds, Google Workspace and the web host. Before publishing, fill in the retention periods, what Google Workspace and the intake form store, transfer safeguards, a second request method, verification, the cookie and Do Not Sell or Share links, and the publication date.",
 "sections": [
  {"heading":"Who we share it with","body":"We use these service providers. Each receives only what it needs for the purpose given:\n- Calendly - scheduling: your name, email and booking details, so you can book online.\n- Stripe - payments: card details, billing address and fraud signals, to process deposits.\n- Google reCAPTCHA - security: device and interaction data and IP address, to block spam and bots.\n- YouTube (Google) - embedded video: viewing data, cookies and IP address when a video is loaded or played. Google may use this data for its own purposes, including advertising.\n- Google Workspace - [[what Google Workspace is used for and what personal information it holds - for example email, files, calendar]].\n- Our website host - to run the site (technical data such as IP address).\n\nIntake form submissions are sent to and stored in [[where intake form submissions are sent and stored]].\n\nWe may also disclose information where the law requires it. Because YouTube embeds may set advertising cookies, California law may treat this as \"sharing\" - see the California section."},
  {"heading":"How long we keep it","body":"- Intake forms and health information: [[how long intake forms and health information are kept]].\n- Booking records: [[how long booking records are kept]].\n- Payment and deposit records: [[how long payment and deposit records are kept]].\n- Emails and enquiries: [[how long emails and enquiries are kept]].\n\nWhen these periods end, the information is to be deleted or made anonymous."},
  /* ... 11 more: Who we are, What we collect and where it comes from, How and why we use it, Cookies and similar technologies, International transfers, Your rights and how to use them, Children, Marketing email, California privacy rights, Changes to this notice, How to contact us ... */
 ],
 "unknowns": [
  {"fact":"Retention of intake forms and health information","placeholder":"[[how long intake forms and health information are kept]]","why":"The page lists retention as unknown; set a period, taking into account any healthcare record-keeping rules that apply."},
  {"fact":"What Google Workspace holds","placeholder":"[[what Google Workspace is used for and what personal information it holds - for example email, files, calendar]]","why":"Google Workspace is named under Tools but the page cannot see what it is used for or what it collects."},
  /* ... 10 more ... */
 ],
 "implementation": [
  {"item":"Cookie consent and settings control [[link to cookie settings]]","why":"The notice cannot refuse cookies itself; YouTube and Calendly set non-essential cookies, so a control that blocks them until the visitor chooses is needed (and YouTube's privacy-enhanced mode or click-to-load could reduce sharing)."},
  {"item":"Do Not Sell or Share link [[link to the Do Not Sell or Share My Personal Information page]] with Global Privacy Control","why":"YouTube's advertising cookies may be sharing under the CPRA; the site must offer the link and actually honour GPC signals by blocking those tags."},
  /* ... 5 more ... */
 ],
 "findings": [
  {"rule":"NEW","severity":"high","title":"Health-privacy laws need legal review","where":"Forms / intake","why":"A physical therapy practice collecting medical history may be a HIPAA covered entity and is likely subject to California's Confidentiality of Medical Information Act; if HIPAA applies, a separate Notice of Privacy Practices is required. These are not in the page's law list, so the notice does not address them.","fix":"Have counsel confirm HIPAA and CMIA status, and whether a Notice of Privacy Practices and business associate agreements are needed."},
  /* ... 4 more: Sensitive personal information and the right to limit, Canadian provincial laws, Intake form destination unknown, Google Workspace role unknown ... */
 ],
 "prescan": [
  {"rule":"PL-SHARE","call":"agree","note":"YouTube embeds carry advertising cookies; the notice discloses possible sharing and a Do Not Sell or Share link with GPC, which must be built."},
  {"rule":"PL-SENSITIVE","call":"agree","note":"The intake form asks for health information; the notice names it as sensitive, and HIPAA/CMIA and the CPRA right to limit are flagged for legal review."},
  {"rule":"PL-UNKNOWN","call":"agree","note":"Retention is unknown; the notice uses one placeholder per kind of data."}
 ],
 "caveats": [
  "This is a template for informational purposes. Consult with a qualified attorney for legal advice specific to your situation.",
  "Whether the CCPA/CPRA applies depends on its thresholds (revenue, volume of California consumers, or revenue from selling or sharing); counsel should confirm before relying on the California section.",
  /* ... 3 more ... */
 ]
}

Every tool in the inventory is named, Google Workspace with a placeholder for what it holds, and no tool outside it; with no retention given, each kind of data gets its own [[placeholder]], and all twelve are listed under unknowns. The cookie control and the "Do Not Sell or Share" link the notice relies on are placeholders and implementation items, the health-privacy question goes to a NEW finding and the caveats instead of into the notice, and the three warning ids are answered in prescan. The page's reply check finds nothing to note.

Sample 2 - review: a studio's out-of-date notice

A UK design studio with EU and California visitors whose notice, last updated in March 2023, says "We do not use cookies" on a site that loads Google Analytics and the Meta Pixel, and names none of its four tools. The page finds two errors (PL-CONSENT, PL-CONTRA twice), seven missing disclosures and three unnamed tools among its warnings, so the verdict it expects is rework. Input - business, site and policy are exactly what the page sends; page is shortened (send the whole text, about 6,700 characters):

{
 "task": "review",
 "business": "Business: Quenmoor Studio Ltd\nContact: privacy@quenmoorstudio.example\nAddress: 12 Tarrowby Lane, Bristol BS1 4XX, United Kingdom\nWebsite: https://quenmoorstudio.example\nWhat we do: brand and web design for small businesses\nUsers in: UK, EU, US (California)\nChildren: no, business clients only\nNewsletter: yes, monthly\nAccounts: no\nPayments: no, we invoice\nRetention: enquiries 2 years; newsletter until unsubscribe\nDPO: none",
 "site": "link https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap\nscript https://www.googletagmanager.com/gtag/js?id=G-7QX2K4PL9M\nscript window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-7QX2K4PL9M');\nscript ...t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','118204477391');fbq('track','PageView'...\nform /contact\nform https://quenmoor.us21.list-manage.com/subscribe/post?u=4f1c2&amp;id=9a8b7\nfield name (text name Your name)\nfield email (email email Email)\nfield message (textarea message Tell us about your project)",
 "policy": "Privacy Policy\nLast updated: 3 March 2023\n\nQuenmoor Studio respects your privacy. This policy explains what information we collect when you use our website and how we use it.\n\nInformation we collect. When you contact us through our website we collect your name, your email address and the details of your project, so that we can reply to you and prepare a proposal. If you sign up to our newsletter we collect your email address.\n\nCookies. We do not use cookies on this website.\n\nHow we use your information. We use your information to respond to enquiries, to provide our services and to send our newsletter if you have asked for it. You can unsubscribe at any time using the link in every email.\n\nSharing. We do not sell your personal information. We use trusted service providers to run our website and send our newsletter.\n\nYour rights. You can ask us for a copy of the information we hold about you, or ask us to correct or delete it, by emailing hello@quenmoorstudio.example.\n\nContact us. If you have any questions about this policy, email hello@quenmoorstudio.example.",
 "today": "2026-10-30",
 "page": "Today: 2026-10-30.\n...\nLaws whose disclosures apply: GDPR and UK GDPR; PECR (cookies) and the ePrivacy Directive (cookies); CCPA / CPRA (if over its thresholds); CAN-SPAM.\nTools (name | category | what it collects | non-essential cookies | US transfer | advertising | found):\n- Google Analytics | analytics | device and usage data, IP address, cookie identifiers | yes | yes | no | script https://www.googletagmanager.com/gtag/js?id=G-7QX2K4PL9M\n...\nDisclosures required (id | law | what):\n- controller | GDPR / UK GDPR | Who is responsible for the data (the controller) and how to contact them\n...\n- bases | GDPR / UK GDPR | The lawful basis for each purpose (consent, contract, legal obligation, legitimate interests)\n...\nUnknown facts (leave a [[placeholder]], never invent): safeguard - the transfer safeguard each US provider uses (the Data Privacy Framework or contractual clauses).\nThe current notice: 184 words, last updated 2023-03-03.\nDisclosures in it (id | found):\n- controller | found\n...\n- bases | MISSING\n...\nTools it names: none.\nStatements the inventory contradicts:\n- \"We do not use cookies\" - says no cookies are used, but the site loads tools that set them\n- \"We do not sell your personal information\" - says personal data is not sold or shared, but advertising tags send it to ad platforms - under the CPRA that is sharing\nFindings (rule | severity | where | text):\n- PL-CONSENT | error | Cookies | Non-essential cookies or trackers with EU or UK visitors (Google Analytics, Meta Pixel)\n- PL-CONTRA | error | Notice | The notice says something the site contradicts (says no cookies are used, but the site loads tools that set them)\n- PL-CONTRA | error | Notice | The notice says something the site contradicts (says personal data is not sold or shared, but advertising tags send it to ad platforms - under the CPRA that is sharing)\n..."
}

Reply - the saved reply, abbreviated as above:

{
 "task": "review",
 "verdict": "rework",
 "summary": "Rework: the notice says the site uses no cookies and does not sell data, but the site loads Google Analytics and the Meta Pixel, which set cookies and, under the CPRA, share data with an ad platform. Correct the cookies statement first and put a consent banner in front of both tags. Beyond that the notice names none of the four tools, is missing seven of the sixteen required disclosures (lawful basis, transfers, retention, complaint, and the three California rights items) and was last updated in March 2023.",
 "contradictions": [
  {"quote":"We do not use cookies on this website.","why":"The site loads Google Analytics (gtag.js, G-7QX2K4PL9M) and the Meta Pixel (fbevents.js), and both set non-essential cookies and identifiers in the visitor's browser.","fix":"Replace it with a cookies section that names Google Analytics (measuring how the site is used) and the Meta Pixel (measuring and targeting advertising), says they only run once the visitor consents, and explains how to refuse or withdraw consent through [[link to cookie settings]] and browser settings. Google Fonts loads from Google's servers and receives the visitor's IP address and browser details; say so in the same section."},
  {"quote":"We do not sell your personal information.","why":"The Meta Pixel sends page views, events, advertising identifiers and IP addresses to Meta for advertising. Under the CPRA that counts as sharing personal information for cross-context behavioural advertising, so a notice that only denies selling leaves out the sharing.","fix":"Say that the site uses the Meta Pixel for advertising, that under California law this may count as sharing personal information, and that California residents can opt out through [[Do Not Sell or Share My Personal Information link]] or by sending a Global Privacy Control signal. Do not repeat the claim that data is never sold or shared unless the Pixel is removed."}
 ],
 "gaps": [
  {"requirement":"bases","law":"GDPR / UK GDPR","fix":"Add the lawful basis for each purpose.","clause":"Our lawful bases. We reply to your enquiry and prepare a proposal because you asked us to, which is a step before entering into a contract (or our legitimate interest in answering enquiries about our services). We send our newsletter only with your consent. We use Google Analytics and the Meta Pixel only with your consent, given through our cookie banner. We load fonts from Google Fonts on the basis of our legitimate interest in showing the website correctly [[confirm the basis for Google Fonts with your adviser, or host the fonts yourself]]. Where we rely on consent you can withdraw it at any time."},
  {"requirement":"retention","law":"GDPR / UK GDPR, CPRA","fix":"Give a period for each kind of data, using the facts you have and placeholders for the rest.","clause":"How long we keep your information. We keep enquiries sent through our contact form for 2 years. We keep your newsletter subscription until you unsubscribe. Analytics data in Google Analytics is kept for [[Google Analytics data retention setting]]. Data collected by the Meta Pixel is kept by Meta under its own terms for [[how long Meta Pixel data is kept - check Meta's terms]]. Information about clients we go on to work with is kept for [[how long client and contract records are kept]]."},
  /* ... 5 more: transfers, complaint, ccpa-rights, ccpa-optout, ccpa-requests ... */
 ],
 "tools": [
  {"tool":"Google Analytics","fix":"Name it in the sharing and cookies sections: it collects device and usage data, IP address and cookie identifiers to measure how the site is used, runs only after consent, and is operated by Google in the US."},
  {"tool":"Meta Pixel","fix":"Name it in the sharing and cookies sections: it collects page views, events, advertising identifiers and IP address for advertising, runs only after consent, is operated by Meta in the US, and is the reason for the California Do Not Sell or Share section."},
  /* ... 2 more: Mailchimp, Google Fonts (fonts) ... */
 ],
 "keep": [
  "It says what is collected through the contact form (name, email, project details) and why, which matches the form fields.",
  /* ... 3 more ... */
 ],
 "findings": [
  {"rule":"NEW","severity":"medium","title":"Wrong contact email and incomplete controller details","where":"Your rights; Contact us","why":"The notice sends requests to hello@quenmoorstudio.example, but the business facts give privacy@quenmoorstudio.example as the contact, and the notice calls the business Quenmoor Studio without its legal name (Quenmoor Studio Ltd) or its address. The page marks the controller disclosure as found, but the details do not match the facts.","fix":"Name Quenmoor Studio Ltd, 12 Tarrowby Lane, Bristol BS1 4XX, United Kingdom, as the controller and use privacy@quenmoorstudio.example throughout, or confirm that hello@quenmoorstudio.example is monitored for privacy requests."},
  /* ... 3 more: GDPR rights list is incomplete, Withdrawal of consent covers only the newsletter, Possible EU representative ... */
 ],
 "prescan": [
  {"rule":"PL-CONSENT","call":"agree","note":"Google Analytics and the Meta Pixel load unconditionally in the page source, so UK and EU visitors get non-essential cookies without consent. This must be fixed on the site with a consent banner that blocks both tags until the visitor agrees; changing the notice alone does not fix it."},
  {"rule":"PL-CONTRA","call":"agree","note":"Both contradictions are real: 'We do not use cookies' is false because of Google Analytics and the Meta Pixel, and 'We do not sell your personal information' leaves out the sharing the Meta Pixel does under the CPRA."},
  /* ... 5 more: PL-SHARE, PL-TRANSFER, PL-STALE, PL-MISSING, PL-UNLISTED ... */
 ],
 "caveats": [
  "This is a template for informational purposes. Consult with a qualified attorney for legal advice specific to your situation.",
  /* ... 3 more ... */
 ]
}

The verdict matches the page's findings; both contradicted statements are quoted from the notice and corrected; each of the seven missing disclosures has a suggested clause, with placeholders where the facts stop (the transfer safeguards, the Google Analytics retention setting) and the facts' own retention periods where they exist; all four tools are named under tools; and the seven distinct error and warning ids are answered once each in prescan. The page's reply check finds nothing to note.

Build the input yourself

The inventory and the reply checker are plain scripts that run under node (global.window = global; require("./plscan.js"); require("./plreply.js"), from the app at https://privacy-ledger.skillsafe.ai/plscan.js and /plreply.js). The same facts, HTML, notice and today give the same site and page as the browser - both samples above reproduce byte for byte:

const S = PlScan, R = PlReply;
const st = { business: fs.readFileSync("facts.txt", "utf8"), html: fs.readFileSync("index.html", "utf8"), policy: "", today: "2026-10-30" };
const b = S.buildInput(st, "draft");               // or "review" with st.policy; b.ok is false (with b.note) when it cannot run
console.log(b.scan.vendors.map(v => v.name), b.scan.findings.map(f => f.id)); // [ 'Stripe', ... ] [ 'PL-SHARE', ... ]
const wire = S.toWire(b.input);                    // task, business, site, policy, today, page - all strings; POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "draft");
// R.check(reply, b)                               // {notes, warn}: missing disclosures, unnamed or extra tools, invented facts, unanswered findings
// R.md("Brackwen Physio", b, reply)              // the inventory, the findings and the notice as Markdown

Without node, the page itself shows the same inventory: paste the facts and the HTML at privacy-ledger.skillsafe.ai, and the ledger it shows - tools, required disclosures, unknown facts and findings - comes from the same scan as the page text it sends. Get a token for the steps below from /tokens.html.

Step 1 - a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html in place of YOUR_TOKEN, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://privacy-ledger.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 - a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"privacy-ledger"}' | jq -r '.data.token')

Step 3 - GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}'

Step 4 - POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra. Save a worked-sample input above as input.json (task "draft" with the facts, the site lines and the page, or "review" with the notice as policy too, each with the whole page the page builds for it) to follow along.

# input.json: the run input object - {"task": "draft", "business": "Business: ...", "site": "script ...", "policy": "", "today": "2026-10-30", "page": "Today: ...\nLaws ...\nFindings ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'

Step 5 - POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses privacy-ledger:<task>:<inputHash>:a<attempt> - the lane is part of the key, so a draft and a review are two runs - and its one automatic reformat retry (sent with a retry_note, which the hash leaves out) is a new attempt number, a2, named in the page as one extra run. Any stable hash of the input works for your own key. Each run is metered on gpt-terra; the job reports charged_credits, and truncated is true when the available balance cut the reply short. The code below parses the JSON object out of the output and prints, for "draft", the summary, each section under its heading, the placeholders to fill in and what the notice cannot do by itself; for "review", the verdict, the summary, the contradictions, the gaps, the tools to name and what to keep; then for both lanes the page's calls, the further points and the caveats.

KEY="privacy-ledger:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '(if .task == "review" then
    "VERDICT: \(.verdict)", .summary,
    (.contradictions[]? | "! \"\(.quote)\" - \(.fix)"),
    (.gaps[]? | "gap \(.requirement) (\(.law)): \(.fix)"),
    (.tools[]? | "name \(.tool): \(.fix)"),
    (.keep[]? | "keep: \(.)")
  else
    .summary, "",
    (.sections[]? | "## \(.heading)", .body, ""),
    (.unknowns[]? | "fill in \(.placeholder) - \(.why)"),
    (.implementation[]? | "outside the notice: \(.item) - \(.why)")
  end),
  (.prescan[]? | "\(.rule) \(.call): \(.note)"),
  (.findings[]? | "\(.rule) [\(.severity)] \(.title) - \(.fix)"),
  (.caveats[]? | "caveat: \(.)")' reply.json

Step 6 - POST /run-stream

The same run as an SSE stream, with the same input.json and the same kind of key. Browsers receive progress ticks rather than text deltas; the final done message carries the job, whose output.output is the reply.

# SSE: tick/delta messages while the job runs, then a final message with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: privacy-ledger:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json