Public API reference
Every route below is unauthenticated, keyless, CORS-open for GET, rate-limited per IP, and
served from published artifacts. There is no database behind any of them: the read plane is
a set of static documents on a content delivery network, which is why a hundredfold traffic
spike is a cache statistic rather than an incident.
Base origin: https://api.purposesource.org. Machine endpoints are also mounted on the apex
(/jwks.json, /stats.json, /ct/*).
Conventions
Section titled “Conventions”-
Versioning.
/v1prefix, additive-only. A breaking change gets a new versioned URL; an existing one is never mutated under you. -
Caching. Every artifact response carries
ETagandLast-Modified(the artifact’sgeneratedAt) and honoursIf-None-Matchwith304. Poll with conditional requests — it is the cheapest thing you can do and it is what these routes are designed for. -
Freshness. Eventually consistent by design. The published propagation bound for coverage-relevant state is five minutes worldwide. No endpoint promises real-time.
-
Errors. One envelope, always:
{ "error": { "code": "cert_not_found", "message": "…", "traceId": "…" } }Codes are stable, additive-only, and lowercase snake case:
invalid_repo_id,invalid_company_ref,repo_not_registered,company_ambiguous,cert_not_found,cert_not_in_ct_log,cert_revoked,artifact_not_found,sandbox_artifact_on_prod,turnstile_failed,rate_limited,artifacts_unavailable,internal.
Route set at v0
Section titled “Route set at v0”| Route | Method | Notes |
|---|---|---|
/badge/{node_id}.json | GET | Shields endpoint schema |
/v1/verify/{certId} | GET | Certificate status record, including the full signed token |
/jwks.json | GET | Public key set (production) |
/stats.json | GET | Public counters |
/v1/meta | GET | The deployment’s self-description |
/v1/forms/{contact|abuse|dsar} | POST | Challenge-protected intake; stores nothing |
/v1/registry/*, /v1/waivers/*, /v1/entitlements/{co}.jws, /v1/ledger/*, /ct/* | GET | Byte-exact passthrough of committed artifacts |
Not available at v0: the computed coverage endpoint and the change feed. The v0 proof of coverage is the signed entitlement record plus the verification page. Both routes activate in a later phase with no schema change.
GET /badge/{node_id}.json
Section titled “GET /badge/{node_id}.json”Shields.io endpoint schema, so a repository readme can embed it directly:
{ "schemaVersion": 1, "label": "purpose source", "message": "registered", "color": "brightgreen", "cacheSeconds": 3600}An unknown repository answers 200 with {"isError": true, …, "message": "not registered"} —
shields renders errors from bodies, not from status codes, so a 404 would render as a broken
image instead of an honest label.
A repository that is delisted, suspended, or has quit answers the neutral form
("message": "status: see registry", grey). A stale badge never keeps asserting registration:
the worker cross-checks the delist set on every request, so the neutral form wins even against
a cached badge artifact.
Cache: s-maxage=3600, stale-while-revalidate=86400, stale-if-error=604800.
GET /v1/verify/{certId}
Section titled “GET /v1/verify/{certId}”Passthrough of the certificate record:
{ "schemaVersion": 1, "generatedAt": "2027-03-02T10:00:00Z", "cid": "cert_01jf8w2c9km3q7xz5r0v4t6y8a", "status": "revoked", "typ": "supporter", "variant": "entitlement", "sub": "Example Industries AG", "scope": { "kind": "project", "repos": ["R_kgDOAbc123"] }, "band": "10-100M", "period": { "validFrom": "2027-01-01", "validUntil": "2027-12-31" }, "issuedAt": "2027-01-01T09:00:00Z", "kid": "psn-prod-2027-1", "ct": { "seq": 118234, "segment": 11 }, "revocation": { "reasonClass": "project-delisting", "at": "2027-03-01T12:00:00Z" }, "supersedes": "cert_01he…", "supersededBy": null, "jws": "eyJhbGciOiJFUzI1NiIs…"}status is one of valid, expired, revoked, superseded. ct is null for a
signed-but-unlogged certificate, and a client that sees null must render it as unverified.
The record never contains more subject data than the certificate itself displays.
Verify it yourself. The record is not the proof; the signature is. Fetch /jwks.json,
select the key by kid, and verify the jws locally — that is exactly what the
verification page does in your browser, with no server in the trust path.
GET /jwks.json
Section titled “GET /jwks.json”The production key set. Retired keys stay published with their validity windows, because
rotation must never invalidate a certificate that was validly issued. The sandbox key set is a
disjoint document at a separate path, and a sandbox key id (psn-sandbox-…) presented on a
production route is an error, never a downgrade.
GET /stats.json
Section titled “GET /stats.json”{ "schemaVersion": 1, "generatedAt": "2026-09-01T05:00:00Z", "state": "pre-launch", "projectsRegistered": 0, "contributorsClaimed": null, "companiesCovered": 0, "chfRoutedMinor": null, "cur": "CHF", "firstDisbursementScheduledFor": null, "loi": null, "detectedUnclaimed": 0, "smallnessThresholds": { "projects": 25, "contributors": 50, "companies": 10 }}state is one of pre-launch, launched-pre-disbursement, post-first-franc, computed from
ledger and registry facts — never set by hand. A null numeric field means “not measurable
yet” and must not be rendered as zero. Money is integer minor units plus a currency code.
GET /v1/meta
Section titled “GET /v1/meta”The deployment’s self-description, used by the specification cross-check and by status probes:
{ "edgeVersion": "…", "routes": ["badge", "verify", "jwks", "stats", "meta", "forms", "artifacts"], "coverage": { "algoVersion": "cov-v1", "available": false }, "contracts": { "answerEnum": ["…the eight coverage values…"], "schemaVersions": {} }}POST /v1/forms/{contact|abuse|dsar}
Section titled “POST /v1/forms/{contact|abuse|dsar}”Challenge-protected intake. The worker verifies the challenge token server-side, forwards the
payload as one transactional email, and stores nothing. Response: 202 with
{ "ack": true, "responseTarget": "5 business days" }. A failed challenge is 403 turnstile_failed. Rate limit: five per hour per IP, on top of the challenge.
Organisation names are sent by POST, never in a query string, so a third party’s name never appears in a shareable or cached URL.
Coverage answers (documented now, computed later)
Section titled “Coverage answers (documented now, computed later)”The coverage answer set is closed at exactly eight values, and it is worth reading before you build against it — the enumeration is frozen, and a client that switch-cases over these eight will not need changing when the endpoint activates:
yes-via-pass, yes-via-project, yes-via-portfolio, yes-via-waiver, yes-via-donation,
no, lapsed-in-grace, no-entitlement-required-under-threshold.
Semantics, precedence, and the boundary cases are on the coverage page.
Rate limits
Section titled “Rate limits”| Route class | Limit |
|---|---|
/badge/* | 600/min |
/v1/verify/* | 30/min |
| key set, stats, meta | 60/min |
| artifact passthrough | 60/min |
/v1/forms/* | 5/hour |
A 429 carries the standard error envelope and a Retry-After header. Poll with
If-None-Match: a 304 is cheap for both of us and is what these routes are shaped for.
The sandbox
Section titled “The sandbox”A sandbox tenant is specified and not yet deployed. When it activates it will be a disjoint tenant, not a flag on these routes, because the failure mode being designed out is a test artifact being mistaken for a production credential:
- Separate paths:
/sandbox/v1/coverage,/sandbox/v1/verify/{certId},/sandbox/v1/registry/*, and/sandbox/jwks.json. - Identical schemas and the identical coverage enumeration, so a client tested against the sandbox needs no change in production.
- Disjoint key sets. Sandbox key ids are
psn-sandbox-…; no route ever merges the two key sets, and a sandbox key id presented on a production route is400 sandbox_artifact_on_prod— an error, never a downgrade. - Sandbox signatures never enter the transparency log, deliberately. That is what makes a test artifact detectable as one.
- Every sandbox response is marked: top-level
"sandbox": truein the body and anX-PSN-Sandbox: 1header. The verification page banners a sandbox certificate as a test artifact before it says anything else about it.
Until the tenant exists, there is nothing to point a test suite at, and this page says so rather than publishing a base URL that answers nothing.