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"
}| Field | Required | Notes |
|---|---|---|
amount | no | Omit to let the payer choose — a tip, a donation, an open invoice |
currency | with amount | ISO 4217 |
accepts | no | Narrows what the payer may use; defaults to the address's standing intent |
fee_disclosure | no | Travels inside the signature, so it cannot differ from what was shown |
order_reference | no | Your own key. Returned to the payer |
expires_at | no | No 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 RESPONSIBLEA 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
| Code | Status | Meaning |
|---|---|---|
path.request.expired | 410 | Past its expiry — ask for a new one |
path.request.revoked | 410 | Revoked by the issuer |
path.core.not_found | 404 | No 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
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.