Skip to content
Apier.no
Guides

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:

  1. send it to POST /api/v1/public/receipts/verify, or
  2. 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.json

where 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_check is verified when the signature matches one of Apier's published keys, missing when it does not, and unavailable when Apier could not load its own key set (retry later). On missing, failure_reason says why: signature_mismatch (the payload or signature was changed), unknown_key (no published key has the receipt's kid) or unsupported_alg.
  • key_status is current or retired. A retired key stays published for good, so a receipt signed before a key rotation still verifies, and key_retired_at gives the date the key stopped signing.
  • signed_at is the signed issued_at claim, given only when the signature verified.
  • receipt_log.status is verified when the receipt's hash (receipt_log.entry_hash, the SHA-256 hex of the receipt string) is in the public receipt log, with its seq, kind, logged_at and the first anchor_date whose daily anchor covers it (null until the next anchor). It is missing when the hash is not logged, and it never changes signature_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.

Building with an LLM? Read llms.txt for agent-oriented integration guidance.