For developers
An open request format, and an API behind it.
The interesting surface is not our API. It is the message the merchant issues and the customer's wallet verifies — which is open, versioned and independent of both companies' private code.
API-first
Every surface a store sees — the register app, the operations console — is a client of the same HTTP API. There is no private path that the API cannot do. A POS integration issues a request, subscribes to its state, and reads the reconciliation record for the day.
Requests are tenant-bound per call. Actions are idempotent, scoped by tenant, actor, request and action, so a retried call cannot produce a second payment request or a second refund proposal.
The open payment request protocol — the format any wallet implements — is published at /protocol. API documentation for the rest of this surface will be published at docs.proxypay.app; it is not live yet, and during the pilot it is shared directly. Write to sales@proxypay.app for access.
What the API cannot do
There is no signing endpoint, no transaction-building endpoint and no endpoint that moves money. The API issues requests, reports observations, and records proposals and decisions.
The open payment request
A request is UTF-8 JSON whose exact bytes are canonical (RFC 8785 JCS), signed with Ed25519 over the unsigned claims. The wire form is bounded: 4,096 bytes, four levels of nesting, a fixed set of top-level fields. The full specification, JSON schema and downloadable conformance fixtures are published at /protocol.
| Field | Meaning |
|---|---|
| version | Protocol version. A wallet rejects anything it does not implement. |
| issuer | The exact HTTPS identity that signed the request, matched against the wallet’s own allowlist. |
| key_id | Which published issuer key signed it. Revoked keys are rejected even when the signature verifies. |
| request_id | A random UUID. It carries no customer, member, receipt or product information. |
| payment_reference | A random 32-byte value used only to correlate the public transaction with the request. |
| merchant / location | Bounded display assertions, shown to the customer before approval. |
| recipient | The merchant-controlled receiving address. |
| network / mint | Checked against the wallet’s own trusted configuration, never inferred from the request. |
| amount_base_units | A positive canonical integer string. No floats, no decimals, no leading zeros. |
| issued_at / expires_at | Whole-second instants. Validity duration and clock skew are both bounded. |
| nonce | Random, reserved against replay for the life of the request. |
| display_reference | A short non-sensitive label a human can read back at the counter. |
| signature | Ed25519 over the canonical bytes of the other claims, unpadded base64url. |
// A wallet validates in a fixed order, and the FIRST failure decides the error.
local policy -> wire size, depth, duplicate names
-> JSON parse and exact re-canonicalisation
-> unknown/prohibited/missing/typed fields
-> claim semantics (issuer, network, mint, decimals, amount, time)
-> issuer metadata and key state (active, unexpired, unique)
-> Ed25519 signature
-> atomic replay reservation of request_id, reference and nonce
-> DISPLAY EVERY CRITICAL FIELD, then a separate customer approval
Rules the format enforces
- The raw bytes are authoritative. Outer whitespace is a rejection, not something to trim on paste or deep-link routing.
- Duplicate top-level JSON names are rejected before canonicalisation, rather than relying on a parser’s last-name-wins behaviour.
- Money is always a whole number of base units. A wallet verifies six decimals from its own configuration and never infers them from the request.
- A caller may tighten the protocol ceilings. It may never loosen them.
- Customer identity, contact details, loyalty, item or basket data, memos and receipt identifiers are prohibited fields, not discouraged ones.
- Funding a wallet can never approve or submit a transfer. Approval is always a separate, explicit customer action.
Any compatible wallet
The format is open and versioned independently of any one wallet. A wallet copies the conformance bytes and implements verification itself; it does not import ProxyPay code or call a ProxyPay private service. Every wallet that implements it is treated the same way, including the one built by the company named in this site’s footer disclosure.
Failure is a taxonomy, not a boolean
| Family | Examples |
|---|---|
| Policy and preflight | Bad local policy, wire too large, nesting too deep, envelope shape limit, duplicate JSON name. |
| Parse and canonicalisation | Malformed UTF-8, malformed JSON, non-object envelope, non-canonical wire bytes. |
| Shape and privacy | Unknown field, prohibited field, missing field, wrong type, unsafe display value. |
| Policy and time | Unsupported version, issuer or network or mint mismatch, malformed amount, not yet valid, expired, validity too long. |
| Metadata and key | Malformed key material, unknown or ambiguous key, inactive key, revoked key, stale metadata cache. |
| Signature | Malformed signature or public key, signature invalid. |
| Replay | Duplicate request identifier, reference or nonce; replay store exhausted. |
Clients may show the customer something coarser than the internal code, but the internal code is exact and the precedence between families is pinned by tests, so two implementations disagreeing about a bad request is a bug with an address.