GitHub

API reference

Payment requests

Issuing a request, reading one as any wallet, and closing it out.

Creation and closure need a member credential. Reading does not — that is what lets a wallet at another institution pay it.


POST /requests

{
  "amount": "5000",
  "currency": "XOF",
  "accepts": [{ "rail": "mobile_money", "currency": "XOF" }],
  "fee_disclosure": {
    "network_fee": "0",
    "operator_fee": "25",
    "currency": "XOF",
    "total_deducted": "25"
  },
  "order_reference": "ord_10482",
  "expires_at": "2026-09-10T10:00:00Z"
}
{
  "reference": "7fk2m9pq3vx8",
  "url": "https://api.example.com/p/7fk2m9pq3vx8",
  "status": "created",
  "expires_at": "2026-09-10T10:00:00Z"
}
FieldRequiredNotes
amountnoOmit to let the payer choose — a tip, a donation, an open invoice
currencywith amountISO 4217
acceptsnoNarrows what the payer may use; defaults to the address's standing intent
fee_disclosurenoTravels inside the signature, so it cannot differ from what was shown
order_referencenoYour own key. Returned to the payer
expires_atnoNo expiry means indefinite, which is rarely what you want

Idempotency: send Path-Idempotency-Key. A replay returns the original request rather than creating a second one — which matters most on the retry after a timeout, when you do not know whether the first call landed.

References are random. A sequential reference leaks volume: read two links a week apart and you know how many payments were requested in between.


GET /requests/{reference}

Public. No credential. This is the route a foreign wallet calls.

{
  "kind": "payment_request",
  "reference": "7fk2m9pq3vx8",
  "issuer": "example-member",
  "amount": "5000",
  "currency": "XOF",
  "accepts": [{ "rail": "mobile_money", "currency": "XOF" }],
  "fees": { "network_fee": "0", "operator_fee": "25", "currency": "XOF" },
  "order_reference": "ord_10482",
  "status": "created",
  "expires_at": "2026-09-10T10:00:00Z",
  "protocol_version": "0.1.0",
  "signed_at": "2026-09-09T10:00:00Z",
  "kid": "op_example_2026_01",
  "signature": "…"
}

The same shape as a resolver answer with the terms already fixed. A wallet that reads one reads the other with no new code.

Verify before displaying

The domain says      WHERE TO FETCH
The signature says   WHO IS RESPONSIBLE

A link may be served from the issuer's domain, from its network under a managed arrangement, or from a custom domain. Fetch the issuer's keys from its discovery document, check the kid, then show the issuer's verified name — never the raw URL. A payer confronted with an unfamiliar hostname closes the app, and a card terminal shows a merchant name rather than an acquirer's for the same reason.

Errors

CodeStatusMeaning
path.request.expired410Past its expiry — ask for a new one
path.request.revoked410Revoked by the issuer
path.core.not_found404No such reference

Expired and revoked are distinguished from not-found on purpose: the payer needs to know whether they are too late or looking at the wrong thing.


POST /requests/{reference}/paid

{ "reference": "7fk2m9pq3vx8", "status": "paid" }

Separate from issuing a receipt, because payment and proof are two events that can be seconds or hours apart depending on the rail. Conflating them forces every integration to pretend a mobile-money transfer settles as fast as a signature.

Re-marking an already-paid request is not an error, and does not append a second transition — a duplicate webhook must not rewrite history.


POST /requests/{reference}/revoke

{ "reference": "7fk2m9pq3vx8", "status": "revoked" }

Only the issuer may revoke. A payer opening the link afterwards is told it was revoked rather than that it never existed.


Lifecycle

Fig. 01 — Lifecycle
created
paid
expired
revoked

created is the only state with anywhere to go. Every transition is recorded — a status overwritten without a trail cannot be contested, and disputes are the case that needs one.

On this page