Mandates, subscriptions and the cap — how a chain with no direct debit still bills monthly.
A blockchain has no direct debit. Nobody can pull from a wallet; a transfer is always pushed by whoever holds the key. That is the whole problem recurring billing has to solve here, and it is solved with an on-chain mandate.
Ask, do not assume. GET /v1/chains publishes subscriptions_available per chain, and GET /v1/merchant/chains publishes the same flag on the chains you have configured. Opening a mandate or a subscription on a chain where it is false returns a 400 saying so.
It is a separate answer from escrow_available, and the two genuinely differ: a chain can be closed to recurring billing while still taking one-time payments perfectly well. Every charge in a subscription is an on-chain transaction that this gateway pays for, and that cost varies between chains by orders of magnitude — so a deployment may reasonably run payments everywhere and recurring billing somewhere narrower.
Subscriptions that already exist on a closed chain keep being billed. Closing a chain stops new ones; it does not cancel anything.
A mandate is a customer's on-chain permission for you to collect up to a cap, once per period. They sign it once, from a wallet, on a page this gateway hosts.
curl -X POST $PAYGATE/v1/mandates \
-H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{"cap_decimal":"50.00","period":"month","customer":"…","reference":"acct_88"}'
You get an authorization_url. Send the customer there; once nothing is left to sign, the page stays, or closes its tab if the link carries ?close=1 — after a five-second countdown that delay= changes. When they sign, mandate.authorized is raised — and that event is your only report that they did.
The customer funds a vault they control. The escrow can move at most the agreed amount, once per period, to you. Nothing on this API can empty that vault or redirect where the money goes. That is a property of the contract, not a promise from the gateway.
curl -X POST $PAYGATE/v1/mandates/{id}/charges \
-H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
-d '{"amount_decimal":"29.00"}'
Four things can happen:
charge.succeeded — the money left the customer.mandate.insufficient — they cannot cover it. Ask them to top up; the mandate is still valid.mandate.requires_authorization — their approval is spent, which is not the same thing and does not have the same fix. They approved cap × approval_periods, that many periods were charged exactly as designed, and the authorisation reached the end it was sized for. The wallet behind it may be full. Send them back to authorization_url and they sign again on the same mandate — no new mandate, no new checkout, nothing to re-provision. Asking this customer to top up is advice they cannot act on.mandate_cap_exceeded (400) — you asked for more than they signed for. Not a failure of the customer's; a failure of yours.The charge's own failure_code splits the same way: insufficient_funds against approval_exhausted. If you branch on one thing here, branch on that.
charge.succeeded means the money left the customer, not that you have it. Under escrow holds, charge.released is the one that means you have it, and charge.returned is the other ending. Keying fulfilment off the wrong one is how you ship at the wrong moment and never find out why.
A mandate is the authorisation. A subscription is the thing that uses it on a schedule, from a catalogue:
curl -X POST $PAYGATE/v1/subscriptions \
-H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
-d '{"customer":"…","price":"…"}'
Creating one opens the customer's on-chain authorisation and returns an authorization_url to send them to. Each period boundary bills, raises invoice.paid, and moves on.
This is the part that differs from a card, and it changes what your dunning has to say.
A card has no ceiling: a discount ending, a plan upgrade, or a usage overage is just a larger charge. Here it is an amount rising through a limit the customer signed months ago — and raising that limit is a wallet transaction somebody has to be asked for.
So the gateway warns ahead of time:
discount.ending_above_cap — a week before a discount ends into a charge above the cap.usage.limit_exceeded — metered usage above what was authorised is recorded and not charged.schedule.upcoming_charge_at_risk — the next charge will not fit.Handle those three and your customers are asked in time. Ignore them and the first they hear of it is a failed renewal.
In test mode a signature is a call:
curl -X POST $PAYGATE/v1/test/mandates/{id}/fund \
-H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
-d '{"amount_decimal":"500.00"}'
curl -X POST $PAYGATE/v1/test/mandates/{id}/authorize \
-H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" -d '{}'
Then charge it, let it run out of funds, and check that your dunning does something sensible. See Test mode.