Fohrkast public API v1 (/api/in/v1)
Every machine call a business's website, server, scanner or AI gate makes to Fohrkast. Fixed on 19 Oct 2026 before anything printed a path; v1 changes only by adding. Everything Fohrkast prints (the banner script, setup screens, the scanner and the AI gate) uses these paths.
Paths
| Method | Path | Key scope | Test key | What it does |
|---|---|---|---|---|
| GET | /api/in/v1/shield/config/<embed key> | none (public) | n/a | The banner's settings: business name, purposes, notice version |
| POST | /api/in/v1/shield/consent | none; the site's own origin (CORS) | n/a | Records one person's choice from the banner or the hosted page: { recorded, receipt } (Consent receipts, below) |
| POST | /api/in/v1/shield/held | none; the site's own origin (CORS) | n/a | From the banner in report-only, the known trackers it would have held (tracker id, script host), once per browser per IST day; 204, or 409 when the website is not in report-only |
| GET | /api/in/v1/shield/consent?token=&purpose= | consent check | yes | One consent check: { granted, notice, at } (plus chooser, check, ask_again where they apply) |
| GET | /api/in/v1/shield/events?since=<seq> | consent check | yes | Consent given and withdrawn after since, 500 at a time (next, more) |
| POST | /api/in/v1/shield/guardian | consent check | refused | The business's own system confirms or refuses a parent or guardian (Rule 10(1)(a)) |
| GET | /api/in/v1/shield/no-offers?after=<hex> | consent check | yes | The NO OFFERS list: keyed identifiers of people who were erased or withdrew, 1,000 at a time (next, more) |
| GET | /api/in/v1/gate/state | gate | yes | The signed lease and the AI register |
| POST | /api/in/v1/gate/summary | gate | refused | Counts per agent for a window |
| POST | /api/in/v1/gate/events | gate | refused | agent.tripped, tool.changed, gate.local_stop |
| POST | /api/in/v1/gate/log-root | gate | refused | The daily root of the gate's own log |
| POST | /api/in/v1/scan/activate | scanner | yes | Before every run. { version, sha384, nonce } (SHA-384 of the scanner file, 48 random bytes as hex) to { token, expires_in }, below |
| POST | /api/in/v1/scan | scanner | refused | A scan report: labels and counts |
| POST | /api/in/v1/scan/locate | scanner | refused | Where one person's records sit, for a rights request: counts |
| GET | /api/in/v1/scan/notify?incident=I-1A2B3C | scanner | yes | The approved job for one incident: tables, contact columns, channels |
| POST | /api/in/v1/scan/notify | scanner | refused | How many people the list holds and how they can be reached: counts |
The paths before v1 answer exactly the same (one handler each), for sites, scanners and
gates set up before: /api/shield/config/<key>, /api/shield/consent (POST), /api/shield/v1/consent (GET),
/api/shield/v1/events, /api/shield/v1/guardian, /api/gate/v1/{state,summary,events,log-root}, /api/scan,
/api/scan/locate, /api/scan/notify.
Scanner activation
fohrkast-scan reads nothing until Fohrkast activates the run. POST /api/in/v1/scan/activate answers only for a live
scanner key of an active workspace, at most 120 times a key an hour (429), for a version not below Fohrkast's minimum
and a file SHA-384 Fohrkast released (403 otherwise). The token is <base64url payload>.<base64url signature>: payload
v, kh (the key's SHA-256), ws (the workspace), ver, sha (the file's SHA-384), nonce, iat, exp (15 minutes);
the signature is ECDSA P-384 with SHA-384 by Fohrkast's activation key (AWS KMS), which each release carries the public
half of. Every scanner call after it (/scan, /scan/locate, /scan/notify, and their paths before v1) sends the token
in X-Fohrkast-Activation, and is refused with 401 unless the token verifies, has not expired, and names the same key
and workspace; a report whose scanner version differs from the token's is refused too.
Keys
Authorization: Bearer <key>. A key is shown once at creation; Fohrkast keeps only its SHA-256. Each key has one scope
(consent check, gate, scanner, front-desk tablet) and one business.
fk_live_+ 96 hex (384 random bits): may add to the business's record (its hash-chained log).fk_test_+ 96 hex: reads only. A call that would add to the record answers403and records nothing.- Earlier keys (
fk_live_orfk_test_+ 32 hex) work until retired. Keys made before v1 (fks_,fkg_,fkc_,fkd_+ 32 hex) are live.
Errors
JSON { "error": "<short reason>" } with the status: 400 malformed question, 401 unknown, expired or wrong-scope
key, or a scanner call without a valid activation, 403 test key on a call that writes, 404 nothing to answer, 413 body too large, 422 body refused by the
schema (scanner reports add "reason": the error class, never an echo of what was sent). The browser call POST /api/in/v1/shield/consent answers with the status alone
(400, 403 other origin, 404 unknown site, 413).
Consent receipts
receipt in the POST answer is a 384-bit token (96 hex; earlier receipts have 32 hex and still open) for that one record; Fohrkast keeps only its SHA-256. The
person's receipt is <Fohrkast>/c/<embed key>/receipt#w=<receipt>: the token sits in the URL fragment, which no
browser sends to a server, and the page posts it in a request body. It shows that record (purposes and choices,
notice version, rendition id, time in IST, a reference) and lets the person withdraw. It cannot give consent. A
withdrawal there reaches webhooks and the feed as consent.withdrawn, like one from the banner.
Webhook and feed body (v1)
Sent to each registered https endpoint within five minutes of consent being given or withdrawn, retried for about
two days. The events feed returns the same objects.
{
"v": 1,
"type": "consent.given",
"seq": 1042,
"property": "site.shop",
"token_hash": "<SHA-256 of the 16 bytes behind window.Fohrkast.token(), 64 hex>",
"purposes": [{ "purpose": "offers", "granted": true }],
"chooser": "parent",
"at": "2026-10-19T04:00:00.000Z"
}v: this format's version. A new field may appear within v1; a field never changes meaning or disappears.type:consent.givenorconsent.withdrawn.seq: the business's log position (the feed'ssince).token_hashis absent once the person's erasure has started.chooserappears only for a parent or guardian; their consent is sent only once the check confirms it, and their withdrawals carry only the purposes refused.atis UTC (ISO 8601); screens and exports show IST.
Headers: X-Fohrkast-Signature-V2: t=<unix seconds>,v2=<hex HMAC-SHA384 of "<t>.<raw body>" under the endpoint secret>
(check this one), X-Fohrkast-Signature: t=<the same>,v1=<hex HMAC-SHA256 of the same> (kept for receivers built
earlier), X-Fohrkast-Delivery (one id per delivery, kept across retries), X-Fohrkast-Event (the type). Verify against the raw
body; refuse a signature older than five minutes and a delivery id already seen.
NO OFFERS list (v1)
{ "v": 1, "keyed": ["<64 hex>"], "next": "<64 hex>", "more": false }. Each entry is the hex part of a keyed identifier
(k1:<scheme>:<hex>): HMAC-SHA256 under the business's identifier key over
<scheme>:<normalised email or mobile>. A person joins when the business records its own deletion on their erasure request,
or when staff check who is asking on their withdrawal. A marketing export or connector keys its own list the same way and
leaves out every match; Fohrkast never holds the identifier. Kept after erasure pending counsel.