How to verify an apier certificate
Request a signed verification certificate with ?certificate=true, then verify its detached JWS offline against the public JWKS — what's signed, what each field means, and a copy-paste verification script.
[Cite this as: Apier.no Docs v0.1.0 — last updated 2026-06-14]
A verification certificate turns a /verify or /authority answer into a
self-verifiable, signed artifact. Instead of trusting that the JSON you got
really came from Apier, you can check a cryptographic signature against Apier's
public key — with no re-call to /verify or /authority. You still fetch
the public JWKS once (cache it and even that goes away), but the verdict itself
is checked locally. Useful when you need to hand a verdict to a third party (an
auditor, a counterparty, your own archive) who should be able to confirm it
independently.
Request a certificate
Add ?certificate=true to either read endpoint:
curl -H "Authorization: Bearer apier_test_<your_key_here>" \
"https://apier.no/api/v1/company/999999999/verify?certificate=true"The same flag works on /api/v1/company/999999999/authority?certificate=true.
Without the flag (or with certificate=false) the response is unchanged —
the bare verdict you already know. Any value other than true/false returns
400 VALIDATION_FAILED: the flag changes the response shape, so a typo fails
loudly rather than silently handing back the bare result.
The certificate is returned in the response data:
{
"success": true,
"data": {
"result": {
"org_number": "999999999",
"name": "Eksempel AS",
"verification_status": "pass",
"...": "..."
},
"issuer": "https://apier.no",
"issued_at": "2026-06-14T12:00:00+02:00",
"kid": "apier-signing-key-1",
"jwks_uri": "https://apier.no/.well-known/jwks.json",
"rulebook_version": "1.0.0",
"audit_log_ref": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"signature": "<base64url-header>..<base64url-signature>"
},
"_meta": { "...": "..." }
}Every field is reused from the underlying response — the certificate adds no
new verification claim. It only wraps the existing result in a signing
envelope:
| Field | Meaning |
|---|---|
result | The exact /verify or /authority answer, unchanged. |
issuer | Who issued the certificate (https://apier.no). |
issued_at | When it was issued — ISO 8601 with the Europe/Oslo offset. |
kid | The signing key id — matches an entry in the public JWKS. |
jwks_uri | Where to fetch the public key. |
rulebook_version | The rulebook version behind result. |
audit_log_ref | SHA-256 response hash of the wrapped verdict (sha256:…) — identical to the _meta.response_hash of the same query without ?certificate=true. Use it to correlate the certified verdict with that verdict's provenance record. It hashes the bare verdict, not this certificate envelope, so it differs from this response's own _meta.response_hash. |
signature | A detached compact JWS over the canonical certificate (everything except signature itself). |
What is signed
The signature is a detached JWS (RFC 7515 Appendix F): a base64url protected
header ({ "alg": "RS256", "kid": "…", "typ": "apier-verify-cert+jws" }), an
empty middle segment, and the base64url RS256 signature, joined by dots —
header..signature. It covers the canonical JSON of the certificate with
the signature field removed: object keys sorted at every level, no whitespace,
UTF-8. To verify, you reproduce those exact bytes and check them against the
public key published at jwks_uri.
Verify it offline
"Offline" here means the verdict is checked locally against Apier's public
key — there is no re-call to /verify or /authority. You do fetch the public
JWKS once to get that key, but it is cacheable (and selecting the key by kid
keeps old certificates verifiable across key rotation), so steady-state
verification needs no network round-trip. This example uses jose (install it
in your own project) and re-attaches the canonical payload so a standard compact
verify can run:
import { createRemoteJWKSet, compactVerify, base64url } from "jose";
// Canonical JSON — sorted keys, no whitespace (identical to the server).
const canon = (v) =>
JSON.stringify(v, (_, val) =>
val && typeof val === "object" && !Array.isArray(val)
? Object.fromEntries(
Object.entries(val).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
)
: val,
);
// `cert` is the `data` object from a ?certificate=true response.
const { signature, ...claims } = cert;
const [header, , sig] = signature.split(".");
const jws = `${header}.${base64url.encode(canon(claims))}.${sig}`;
const JWKS = createRemoteJWKSet(new URL(cert.jwks_uri));
await compactVerify(jws, JWKS); // throws if the signature does not matchIf compactVerify resolves, the certificate is authentic and untampered. If any
field in claims was altered after signing — a flipped verification_status, a
swapped audit_log_ref — the canonical bytes no longer match and the verify
throws. Selecting the key by kid against the JWKS means a rotated key keeps
old certificates verifiable as long as its public half stays published. Operators
rotate that keypair with the
Maskinporten key rotation runbook, whose
previous-key overlap window keeps a certificate signed under the prior kid
verifiable through the changeover.
Notes
- Same key as Maskinporten discovery. The certificate is signed with the key
whose public half Apier already publishes at
https://apier.no/.well-known/jwks.json. The protected header'styp: "apier-verify-cert+jws"distinguishes a certificate from any other artifact signed with that key. - Nothing extra is stored. A certificate is generated per request from the existing audit record; requesting one persists no new data.
- When signing is unavailable. If the signing key is not configured in an
environment,
?certificate=truereturns503 CERTIFICATE_SIGNING_UNAVAILABLEand the bare-result path is unaffected — drop the flag to get the verdict.
Inspecting actions with the audit trail
Agents have no dashboard. Use the signed receipt, the audit endpoint, correlation_id, and initiated_by to reconstruct exactly what an action did — on the sandbox today, on production later.
Webhook subscriptions
Subscribe to change-archive deltas via signed HTTPS webhooks. SSRF-resistant, exponential-backoff retries, HMAC-SHA256 verification.