Quickstart
Four steps to a paid endpoint. Details are in Reference below.1
Provision a TransferPreapproval (once)
Your party needs a live
TransferPreapproval so incoming payments settle in one transaction. Provision it with the CLI (self-custody wallet), passing the Canton Coin admin (the DSO party, from your Scan API):2
Install the middleware
3
Gate a route
4
Verify
echo "<base64>" | base64 -d | jq '.accepts[0]'How it works
The payer signs aTransferFactory_Transfer naming you as receiver but does not submit it. The facilitator relays it and pays the traffic fee; your standing TransferPreapproval makes it settle directly to you in one transaction. No escrow, no custody. CC and registry tokens use the same flow, differing only by extra.instrumentId. See How Payments Work for the full flow, error codes, and replay protection.
Reference
Facilitator parameters
feePayer and synchronizerId come from GET /supported; the DSO party (for instrumentId.admin) comes from your Canton Scan API.
synchronizerId may be omitted from extra; clients fall back to the advertised value.
TransferPreapproval
Without a live preapproval the transfer resolves to a two-step pendingTransferInstruction, /settle returns invalid_exact_canton_preapproval_missing, and callers get a 402 instead of the resource. A preapproval is time-bounded, so renew it before expiry. There is no fixed maximum; a longer --days prepays a slightly larger one-time fee (about 0.016 CC/day, so 180 days is roughly 3 CC). --expires-at <ISO> overrides --days.
To charge a registry token, provision one preapproval per instrument and pass its registrar as --admin:
CANTON_AGENT_REGISTRY_TRUSTED_PARTIES; USDCx is built in).
Hosted-node merchant: if your party lives on your own Canton node, create the TransferPreapproval through that wallet/validator directly (receiver = your party). No relay or CLI needed, and no custom DAR to install - transfer-factory runs through the standard CIP-56 :TransferInstruction interface.
PaymentRequirements fields
amount: integer string of atomic units (1 CC = 10^10). Compared by value, so"100000000"= 0.01 CC; a mismatch returnsinvalid_exact_canton_amount_mismatch.feePayer: the facilitator party. Clients MUST NOT alter it.instrumentId: the token charged, and authoritative overasset(a display symbol). CC is{ admin: "<DSO-party>", id: "Amulet" }; a registry token is{ admin: "<registrar>", id: "<instrument-id>" }.executeBeforeSeconds: relative deadline (seconds) the client uses to compute the transfer’s absoluteexecuteBefore.memo(optional): a reconciliation tag stamped into the transfer metadata; not validated.
Next.js App Router
"METHOD /path", matched exactly against ${req.method} ${req.path}. Unregistered routes pass through ungated.
Request header size
The paid request carries the signed transfer in theX-PAYMENT header - ~8-10 KB on Canton (larger than other chains, and more if the payment draws on several holdings). Raise your ingress header limit or the paid retry fails with 400/431 before reaching your app:
- nginx:
large_client_header_buffers 4 16k;(default4 8k) - Node.js:
--max-http-header-size=32768(default is a 16 KB total cap)
Security notes
- Your
accepts[]is authoritative. The middleware ignores any amount the client claims; a client cannot pay 0.01 CC for a 1 CC resource by tampering. - The proven payer is on-ledger. The facilitator binds the payer to the signed transfer’s sender, not to any payload field, so a spoofed
payercannot impersonate. - Settlement happens before your handler. If your handler throws after
/settle, the payer already paid. Handle errors gracefully; do not retry settlement. - Keep
payTocorrect. Funds go wherepayTosays. If your party changes, updatepaymentReqand provision a newTransferPreapproval.