Skip to content
Apier
Apier.no
Guides

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:

FieldMeaning
resultThe exact /verify or /authority answer, unchanged.
issuerWho issued the certificate (https://apier.no).
issued_atWhen it was issued — ISO 8601 with the Europe/Oslo offset.
kidThe signing key id — matches an entry in the public JWKS.
jwks_uriWhere to fetch the public key.
rulebook_versionThe rulebook version behind result.
audit_log_refSHA-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.
signatureA 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 match

If 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's typ: "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=true returns 503 CERTIFICATE_SIGNING_UNAVAILABLE and the bare-result path is unaffected — drop the flag to get the verdict.