How to verify an Apier receipt
Check that a verification certificate, evidence pack or notary record was signed by Apier, with no API key and at no cost: one POST call, or fully offline with verifyReceipt() in the TypeScript SDK.
[Cite this as: Apier.no Docs v0.1.0 — last updated 2026-11-02]
Every object Apier signs for you to keep (a verification certificate from
?certificate=true, an evidence pack, a notary record) carries a signature
member: a detached JWS over the rest of the object. You can check that
signature yourself in two ways, and neither needs an API key or costs
anything:
- send it to
POST /api/v1/public/receipts/verify, or - check it offline with
verifyReceipt()from@apier-no/sdk.
Both give the same signature_check when both check against the same
usable keys. They can differ when the keys differ: the endpoint answers
unavailable when it cannot load its own key set, while the SDK checks the
key set you pass it, and a key set cached before a key rotation answers
missing (unknown_key) for a receipt signed by the newer key. Fetch the key
set again before you treat missing with unknown_key as final. The endpoint
also tells you whether the receipt's hash is in the
public receipt log and which daily anchor
covers it.
Step 1 — Split the receipt from its payload
Take the object exactly as you received it. The signature member is the
receipt; everything else is the signed payload.
const { signature, ...payload } = certificate;Do not reformat, reorder or round anything inside payload. Key order does
not matter (both checks sort keys before verifying), but every value must be
the one you received.
Step 2 — Call the endpoint
curl -X POST "https://www.apier.no/api/v1/public/receipts/verify" \
-H "Content-Type: application/json" \
-d @receipt.jsonwhere receipt.json holds { "receipt": "<signature>", "payload": { ... } }.
The body may be at most 256 KB, and any field other than receipt and
payload is a 400 VALIDATION_FAILED. Nothing you send is stored, logged or
echoed back.
Step 3 — Read the answer
signature_checkisverifiedwhen the signature matches one of Apier's published keys,missingwhen it does not, andunavailablewhen Apier could not load its own key set (retry later). Onmissing,failure_reasonsays why:signature_mismatch(the payload or signature was changed),unknown_key(no published key has the receipt'skid) orunsupported_alg.key_statusiscurrentorretired. A retired key stays published for good, so a receipt signed before a key rotation still verifies, andkey_retired_atgives the date the key stopped signing.signed_atis the signedissued_atclaim, given only when the signature verified.receipt_log.statusisverifiedwhen the receipt's hash (receipt_log.entry_hash, the SHA-256 hex of the receipt string) is in the public receipt log, with itsseq,kind,logged_atand the firstanchor_datewhose daily anchor covers it (null until the next anchor). It ismissingwhen the hash is not logged, and it never changessignature_check.
signature_check is a status for one check. For a production receipt,
verified says the object was signed with one of Apier's production keys
and has not changed since. For a sandbox receipt it says only that the object
has not changed since it was signed: the sandbox signing key is public, so
anyone can sign a sandbox receipt, and a verified sandbox receipt never shows
that Apier signed it (Step 6). Either way, it says nothing about whether the
facts in the object are still true today.
Step 4 — Verify offline with the SDK
import { verifyReceipt } from "@apier-no/sdk";
const jwks = await (await fetch("https://www.apier.no/.well-known/jwks.json")).json();
const { signature, ...payload } = certificate;
const result = await verifyReceipt({ receipt: signature, payload, jwks });
// result.signature_check: "verified" | "missing"verifyReceipt() uses only the platform's WebCrypto (Node 20 or later, or a
modern browser) and makes no network call of its own: fetch the key set once,
keep it, and verify as often as you like.
Step 5 — Prove a snapshot record is in its daily anchor
An evidence pack lists archived snapshot records, each with its
anchor_date. To check one record against that day's published anchor
without trusting the pack, compute its leaf hash: SHA-256 over the byte
0x00 followed by the canonical JSON of content_hash, first_observed_at
(ISO 8601 UTC with milliseconds) and org_number. Then ask for the proof:
curl "https://www.apier.no/api/v1/public/anchors/2026-11-01/proof?leaf=<leaf hash>"When inclusion is verified, hash the leaf with each sibling in order as
sha256(0x01 || left || right), with the sibling on the side position
names, and compare the result with merkle_root, which
/api/v1/public/anchors also publishes. missing means the record is not in
that day. unavailable means no proof is given, and unavailable_reason says
why: day_too_large when the day holds more than 200,000 records, more than
this endpoint rebuilds (a retry gives the same answer), or day_not_rebuilt
when the day could not be read in time or did not rebuild to its stored root
(retry later).
Step 6 — Sandbox receipts
A receipt from the sandbox names the sandbox key. The endpoint checks it
against /.well-known/sandbox-jwks.json only and answers
environment: "sandbox"; sandbox receipts are never in the public log, so
receipt_log.status is not_checked. Offline, pass the sandbox key set to
verifyReceipt(). A sandbox receipt never verifies against the production
keys. The sandbox key is public by design, so treat a sandbox receipt as test
data only: its verified proves nothing about who signed it.
How to check a receipt in the public receipt log
Every receipt Apier issues is logged as a hash in a public, append-only hash chain, anchored daily. Check that your receipt was logged, when, and that the log up to its daily anchor was not rewritten, with no API key.
How to check how fresh Apier's sources are
Read the public source health endpoint: last successful fetch, lag, 30-day outage windows, coverage start and licence for every source Apier fetches on a schedule, with no API key.