Blog

Verify any GateCore receipt in your browser

2026-08-24 · GateCore engineering · 5 minute read

Every governed transaction that moves through GateCore ends in a signed action receipt. The receipt binds the identity that acted, the policy decision that allowed it, the execution that followed, and the settlement that paid for it, all under one signature. The part we care about today: anyone can verify one, with no GateCore account, no API key, and no network call to us.

That property matters because the parties who need the evidence are rarely us. A buyer disputing a charge, a seller proving delivery, an auditor reconstructing a quarter, a regulator asking who approved an action: each of them can hold the receipt in one hand and the math in the other, and reach a verdict without trusting GateCore's word or GateCore's uptime.

This post walks through exactly what a verifier checks. If you would rather not write code, gatecoreai.com/verify runs the same checks in your browser tab, entirely client side. Nothing you paste there leaves the page.

The envelope

GET /v1/receipts/{request_id} on a GateCore gateway returns a small JSON envelope:

{
  "receipt":         { ... the signed payload ... },
  "payload_sha256":  "bcc6a803...ae5a14c7",
  "signature":       "oXmIfXVH...E6ieCQ",
  "signing_key_id":  "receipt-20260809-prod",
  "alg":             "Ed25519"
}

The receipt object is the payload. The other four fields tell you how to check it: a SHA-256 hash of the payload's canonical bytes, an Ed25519 signature over those bytes, and the id of the public key that signed them.

Step 1: rebuild the canonical bytes

Signatures cover bytes, not JSON values, and the same JSON object can serialize a dozen ways. So GateCore signs one fixed serialization: keys sorted, no whitespace, non-ASCII characters escaped. In Python it is a one-liner, and it is the exact convention the gateway itself uses:

canonical = json.dumps(receipt, sort_keys=True,
                       separators=(",", ":")).encode("utf-8")

Any language can reproduce this: sort object keys by code point, emit minimal separators, escape non-ASCII as \uXXXX. The browser verifier at /verify is a byte-for-byte port of the same rule, checked against gateway-generated test vectors before it shipped.

One deliberate omission: receipt version 2 payloads contain no floating-point numbers. Float serialization is not guaranteed identical across languages, so floats are banned from signed bytes. Amounts are integers in minor units, scores are integers.

Step 2: check the hash

Hash the canonical bytes with SHA-256 and compare against payload_sha256:

assert hashlib.sha256(canonical).hexdigest() == envelope["payload_sha256"]

If a single byte of the receipt changed after issuance, this fails here.

Step 3: check the signature

The signature does not cover the canonical bytes alone. It covers a domain-separated message: a fixed prefix, a zero byte, then the payload bytes.

message = b"gatecore-receipt-v1" + b"\x00" + canonical

The prefix means a signature over a receipt can never be replayed as a signature over any other GateCore object type; every signed object class in the system gets its own domain. The signature itself is base64url encoded Ed25519.

Getting the key

GateCore publishes its receipt signing keys at an unauthenticated endpoint, on purpose. Anyone verifying a receipt needs the public key and should not need an account to get it:

curl https://api.gatecoreai.com/v1/receipts/keys

{
  "keys": [
    {
      "kid": "receipt-20260809-prod",
      "public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
      "status": "active",
      "not_after": null
    }
  ]
}

Fetch it once and keep it. Verification against a stored copy of the key works forever and needs no network at all, which is exactly what an audit trail should require.

Match the envelope's signing_key_id against a kid in the list. Key rotation adds a new kid; it never invalidates old receipts, so a receipt signed last year still verifies against the key that signed it.

One note for browser authors: the endpoint answers any terminal or server, but it does not answer cross-origin browser requests. That CORS posture is deliberate, which is why our own verify page embeds the currently published key rather than fetching it live, and lets you paste a different key if you want to check against the endpoint yourself.

A complete verifier

Putting the three steps together, with the cryptography package:

import base64, hashlib, json
from cryptography.hazmat.primitives.serialization import load_pem_public_key

envelope = json.load(open("receipt.json"))
keys = json.load(open("keys.json"))["keys"]  # from /v1/receipts/keys

canonical = json.dumps(envelope["receipt"], sort_keys=True,
                       separators=(",", ":")).encode("utf-8")

assert hashlib.sha256(canonical).hexdigest() == envelope["payload_sha256"]

pem = next(k["public_key_pem"] for k in keys
           if k["kid"] == envelope["signing_key_id"])
sig = envelope["signature"]
sig = base64.urlsafe_b64decode(sig + "=" * (-len(sig) % 4))

message = b"gatecore-receipt-v1" + b"\x00" + canonical
load_pem_public_key(pem.encode()).verify(sig, message)  # raises on failure
print("receipt verified")

What a green check proves

  • The receipt was signed by the named GateCore key, under the receipt domain.
  • Not one byte of it changed after signing. Any alteration fails both the hash and the signature.
  • Identity, decision, execution, and settlement were bound together under one signature at issuance time.

Version 2 receipts go one step further: the payload carries request.request_sha256, the hash of the exact bytes the requesting agent signed. If you also hold the original signed request, you can check that the receipt settles the request you think it does, and verify the agent's own signature independently.

Verification is deliberately boring. That is the point: a receipt is only useful as evidence if the party holding it can check it alone, offline, years later, against nothing but published keys and open algorithms. Try it now with the sample receipt at gatecoreai.com/verify.