Recurring payments

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.

Which chains you can bill on

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.

The mandate

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.

Charging it

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:

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.

Subscriptions, if you want the billing run too

A mandate is the authorisation. A subscription is the thing that uses it on a schedule, from a catalogue:

  1. Product — a thing you sell.
  2. Price — an amount and a cadence for it. Immutable: there is no PATCH and no DELETE, because editing an amount in place would silently reprice every existing subscriber the next time billing ran. Repricing means a new price and moving subscriptions onto it deliberately.
  3. Subscription — a customer on a price.
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.

The cap is a ceiling nobody can raise for them

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:

Handle those three and your customers are asked in time. Ignore them and the first they hear of it is a failed renewal.

Rehearsing it

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.