Skip to content
ProxyPay home

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.

01

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.

02

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.

Public fields carried by a payment request
FieldMeaning
versionProtocol version. A wallet rejects anything it does not implement.
issuerThe exact HTTPS identity that signed the request, matched against the wallet’s own allowlist.
key_idWhich published issuer key signed it. Revoked keys are rejected even when the signature verifies.
request_idA random UUID. It carries no customer, member, receipt or product information.
payment_referenceA random 32-byte value used only to correlate the public transaction with the request.
merchant / locationBounded display assertions, shown to the customer before approval.
recipientThe merchant-controlled receiving address.
network / mintChecked against the wallet’s own trusted configuration, never inferred from the request.
amount_base_unitsA positive canonical integer string. No floats, no decimals, no leading zeros.
issued_at / expires_atWhole-second instants. Validity duration and clock skew are both bounded.
nonceRandom, reserved against replay for the life of the request.
display_referenceA short non-sensitive label a human can read back at the counter.
signatureEd25519 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
03

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.

04

Failure is a taxonomy, not a boolean

Families of rejection a wallet reports
FamilyExamples
Policy and preflightBad local policy, wire too large, nesting too deep, envelope shape limit, duplicate JSON name.
Parse and canonicalisationMalformed UTF-8, malformed JSON, non-object envelope, non-canonical wire bytes.
Shape and privacyUnknown field, prohibited field, missing field, wrong type, unsafe display value.
Policy and timeUnsupported version, issuer or network or mint mismatch, malformed amount, not yet valid, expired, validity too long.
Metadata and keyMalformed key material, unknown or ambiguous key, inactive key, revoked key, stale metadata cache.
SignatureMalformed signature or public key, signature invalid.
ReplayDuplicate 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.