Skip to content
ProxyPay home

Open payment request protocol

A signed request format, published for any wallet.

A ProxyPay register issues a payment request. It is a signed, versioned, size-limited message, and any wallet — not only ones built by companies affiliated with ProxyPay — can verify it without asking ProxyPay for permission or calling a ProxyPay private service. This page is that format, published: the specification, the JSON schema, and the fixed conformance fixtures a wallet implementer tests against, each with its exact hash.

01

What a request is

A payment request is UTF-8 JSON whose exact bytes equal RFC 8785 JSON Canonicalization Scheme (JCS) output — canonical, deterministic bytes, not merely valid JSON. It is signed with Ed25519 over the unsigned claims. The wire form is bounded on purpose: 4,096 bytes, four levels of nesting, a fixed top-level field count.

Money is always a positive canonical decimal integer string in the token's base units — no floats, no signs, no leading zeros. A wallet verifies the exact network, token mint and decimal count from its own trusted configuration; it never infers them from the request.

Envelope fields
version, issuer, key_id, request_id, payment_reference, merchant and location display names, recipient, network, token mint, amount in base units, issued-at and expires-at instants, nonce, display reference, and the signature — sixteen fields, no more and no fewer.
Signature
Ed25519, canonical unpadded base64url, over the UTF-8 JCS bytes of the other fifteen claims. The key ID is itself one of the signed claims.
What is prohibited
No customer, membership, receipt, item or loyalty data is a permitted field. Fields carrying it are rejected outright, not merely discouraged, because the request is a payment instruction, not a record of who is buying what.
02

How a wallet verifies a request

Validation runs in one fixed order, and the first failing stage decides the outcome — a wallet does not keep checking after the first failure, and two conforming implementations cannot disagree about which stage a bad request fails at.

  1. Local policy first

    Before reading the request at all, the wallet checks its own configuration is sane: the current time, its issuer allowlist, the expected network and token mint, and its own ceilings for amount size, validity duration, clock skew and metadata cache age. A caller may tighten these ceilings; it may never loosen them.

  2. Wire and shape

    The raw bytes must be at most 4,096 bytes of strict UTF-8, nested at most four levels deep, with no duplicate top-level JSON name — checked before the bytes are even parsed as JSON.

  3. Parse and canonicalize

    The bytes parse as one JSON object, and re-canonicalizing that object must reproduce the exact input bytes. Outer whitespace is therefore a rejection, not something a scanner or a deep link may trim.

  4. Field shape

    Unknown fields are rejected, then missing fields, then fields of the wrong type. Only then are the sixteen permitted fields validated by their individual rules — issuer format, ID patterns, timestamp format, amount digits, and so on.

  5. Claim semantics

    Version is supported, issuer matches an exact allowlisted HTTPS identity, network and token mint match the wallet's own configuration, the amount is a valid positive integer within the digit ceiling, and the validity window is well-formed, not expired, not yet future-dated beyond the allowed skew, and not longer than the ceiling.

  6. Issuer metadata and key

    The wallet validates the issuer's published key set against its own bounded shape, requires an exact issuer match and a fresh cache, finds exactly one key matching the request's key_id, and refuses a revoked key immediately even if the signature below would otherwise verify.

  7. Signature

    The Ed25519 signature is verified over the canonical bytes of the other fifteen claims, using the exact public key published for that key_id.

  8. Replay reservation

    request_id, payment_reference and nonce are reserved atomically, all three, for the life of the request. A duplicate of any one of them fails closed, and an otherwise-invalid request must never consume a reservation.

  9. Display and approval

    Only after every prior stage passes does the wallet display the critical fields to the customer and require a separate, explicit approval. Nothing prior to this step can approve or submit a transfer by itself.

03

How a wallet finds the issuer's keys

Every request names its issuer as an exact HTTPS identity. That issuer publishes its current signing keys at a public GET /v1/issuer-metadata endpoint on its own origin, and a wallet fetches that document to learn which keys are currently valid.

The endpoint is unauthenticated by design: a public key is public, and the only party who needs this document — a wallet verifying a signature it was just handed — holds no operator credential for the issuer's systems. The response is cache-control public, max-age=300, and the document's own expires_at is always exactly 300 seconds after its fetched_at — the protocol caps a client's metadata cache at 300 seconds and a caller may tighten that bound but never loosen it.

An issuer that cannot currently produce a key set answers a distinct unavailable response rather than an empty key list, so a wallet can tell "try again shortly" from "no key matches this request" — those are different failures with different remedies.

  • Each published key carries a key_id, its type and curve (Ed25519 in an OKP JWK), the raw public key bytes, an active-or-revoked status, and the time window the issuer claims for it.
  • A revoked key is rejected immediately, even when its cryptographic signature would otherwise verify.
  • Rotation publishes a new key before it is ever used to sign, and the previous key stays published and verifiable through its own overlap window rather than disappearing the moment a new key takes over.
  • The metadata document defines no signing endpoint, no callback and no private route — it is read-only, and reading it can never move money or approve a transfer.
04

Versioning

This page publishes version 1 of the protocol. The version is a field in every envelope, not an assumption: a wallet that does not implement the version named in a request rejects it rather than guessing at a best-effort interpretation. Within v1, a verifier may tighten its own ceilings — a shorter validity window, a smaller amount limit, a shorter metadata cache — but may never loosen any ceiling this specification sets. A future version, if published, would be a new document at a new path, verified independently; it does not silently change what a v1 verifier accepts.

05

Downloads and hashes

The schema and fixtures are byte-identical to the reviewed source; the specification text is editorially adapted for publication. Verify the hash of anything you download before treating it as conformance material.

Specification and schema
FileSizeSHA-256
v1/issuer-metadata-schema.json3,264 bytes826b8ea33e7e138f8353447834519f75dd025cafacbc307110845e33d9b07808
v1/schema.json2,806 bytese69c1a3b5c4c6498246b5d34e00e079f58b963f6e207049ba97ce03ec3848e23
v1/SPECIFICATION.md8,753 bytes610a8e00246079140617b305963ec9987fe6cdc859667068822f926fa8ffdbe6
Conformance fixtures — synthetic envelopes and issuer metadata, RC0
FileSizeSHA-256
v1/fixtures/envelopes/duplicate-name.json767 bytesc1e5a4f6a0df026f3cf435dc20bf1e514d88eca0576dddcd1e1b8592d6412b5a
v1/fixtures/envelopes/duplicate-nonce.json753 bytes29ed1caeca1bd664470a8179570ad440b068ac61d6316b091317c755e53348c6
v1/fixtures/envelopes/duplicate-reference.json753 bytese8182e551018d767f3262fa4e09e5c2719c49f5f961817a02d4f6459e83dd021
v1/fixtures/envelopes/expired.json753 bytesd165e69911228b3f8d925765ce0acf381af37edb77a3fe4c154f9f300b8786a7
v1/fixtures/envelopes/future-issued.json753 bytesfe75d5bbeb69fa1f9cfcbca3d1104e648c3dc6d4e083f663994fe726ae39c997
v1/fixtures/envelopes/invalid-signature.json753 bytes0ac3e864a451e7439912e9a64afd07a0e94189281f4de3121a21f2838e9ee6bc
v1/fixtures/envelopes/malformed-address.json719 bytesdb32b0ea46bf0c13252c6a857382b9211b66f17e54c80e13360a1c72c903dffa
v1/fixtures/envelopes/malformed-canonicalization.json818 bytesd2fd52cb0d990d2bab8985f02812a728727c862245ba759e9e4f24a145a067e6
v1/fixtures/envelopes/malformed-integer.json747 bytes1109844a2b9f77bbc416f1902c12d811a6a0d6d532673c084bca3cd585ac53a5
v1/fixtures/envelopes/malformed-unicode.json751 bytes3ca2abe6d73620490bde0382a13ea657995c5bb4690e8fbe7c0898f97eb68a5e
v1/fixtures/envelopes/missing-signature.json652 bytesa7e1cbbf7ffa25ce46ddb5717f13d66ab6bf036d187e74e94242de96b9872f3b
v1/fixtures/envelopes/overflow.json761 bytes83ce3bda233743bd0452e2dcdcb2eabd1d1ffc84baec74342bf8fa4a1f231c28
v1/fixtures/envelopes/prohibited-field.json787 bytes6d64e8f06de0e808ab031618734356f3c231d3c634423b8915b9fbeb0213c296
v1/fixtures/envelopes/replay.json753 bytes570952b7793e6a110530e161536fa8520a793acf0303e52de039f43d12e8094b
v1/fixtures/envelopes/revoked-key.json753 bytes570952b7793e6a110530e161536fa8520a793acf0303e52de039f43d12e8094b
v1/fixtures/envelopes/tampered-amount.json753 bytes41bb6d8e85ea65eb4da5bdc0d4e257ecd8dfc25780a4d01e08dd43b069e2fed6
v1/fixtures/envelopes/tampered-recipient.json753 bytes4e875e1f3d7ad05c53a86eca433e94ee32c0d7e9aac127d07ba12b009174bba5
v1/fixtures/envelopes/unknown-field.json769 bytes536f80e4f8c2a280945f11d184131e25241f183859915568622e98649b7a46f7
v1/fixtures/envelopes/unknown-issuer.json745 bytesc25b5472d96a963ff17d56f17b6183c0b5f0e400a0709f092ed6bc9a43ce78cc
v1/fixtures/envelopes/valid.json753 bytes570952b7793e6a110530e161536fa8520a793acf0303e52de039f43d12e8094b
v1/fixtures/envelopes/wrong-mint.json741 bytes911b4383e0867a40f26f46ca7bf680326f729ea3d3d2d77008b686366f3d7fde
v1/fixtures/envelopes/wrong-network.json754 bytes37b1dd7678688312d020c2c94d278144de1bd942c6a76d2efcfacc94c0529a5f
v1/fixtures/issuer-metadata/active.json349 bytes4cb4dc9a90c147c241dc66506eba66585118b7d6ec86a4f8eba0d51c93338917
v1/fixtures/issuer-metadata/revoked.json386 bytesea205ea07e7814fb3ee344d35b1816ad86e101d3e7acfe7705e821dba02fcc9b
v1/fixtures/issuer-metadata/unknown-issuer.json341 bytese964c7c4c589a48208ac93313f4df2612f60e864190da89b37ca2ed5487307b5

What these fixtures are, and are not

These are synthetic, no-money conformance vectors: deterministic bytes used to test that a verifier accepts a valid request and rejects each specific way one can be invalid. They do not touch a real network, a real issuer or real funds, and none of the published files contain private key material — the fixture-generating seed is excluded from this publication.

06

Using the mark

A wallet or a merchant integration that implements this specification and passes the published conformance fixtures may describe itself with the plain sentence below. It is a factual statement about protocol support, not a certification ProxyPay issues or a logo ProxyPay licenses.

Accepts open payment requests (v1)

This page does not claim that any wallet other than the one named in this site's footer disclosure implements this protocol. Any wallet may; implementing it does not require contacting ProxyPay, importing ProxyPay code, or paying ProxyPay anything.