Payment intents
Create a payment intent for each end user who is about to move funds, then hand that user a deposit address unique to them.
Create one per user
A merchant on Canopy is an app, and that app creates a payment intent from its own backend for each end user about to move funds -- never one intent shared across users, and never one created ahead of knowing who is paying.
The intent's deposit address is unique to that user. The destination the intent names is where the net amount lands once the deposit settles.
See payout destinations for what that destination can be, and the gate around naming one.
The create body
POST /api/v1/intents accepts the following keys, all optional, and the schema is strict: any other key returns 400 invalid_request.
merchantReference(optional, up to 128 characters).metadata(optional).priceUnits(optional, a base-unit decimal string: digits only, no leading zero, strictly positive). A per-intent price with floor semantics — a deposit at or above the price settles, and one below it parks until topped up. The advertised minimum becomes the greater of the price and the route's confirmed floor.oneTime(optional boolean; requirespriceUnits, otherwise the create is a named400). Once the received total reaches the price, the intent settles and closes — later status reads show the terminal state, and the payout wallet frees for the next sale.payoutWallet(optional, 1 to 64 characters).payoutNamespace(optional, the enum"eip155"or"solana").payoutChainReference(optional, 1 to 64 characters).payoutTokenAddress(optional, 1 to 64 characters): a configured token address ornativefor the destination chain's native currency. The selected token is pinned to the intent. If omitted, the matching account currency or configured chain default is used.
curl -X POST https://www.canopypay.io/api/v1/intents \
-H "Authorization: Bearer cnpy_sk_live_a1b2c3_<64 hex chars>" \
-H "Canopy-Version: 2026-09-01" \
-H "Content-Type: application/json" \
-d '{
"merchantReference": "order_10031",
"payoutWallet": "0x9f2a1c7b3e5d8a604c1f2b9e7d3a5c8b6e4f1d20",
"payoutNamespace": "eip155",
"payoutChainReference": "8453"
}'The four payout keys name a per-intent settlement destination, and the destination is write-once once the intent exists. Naming one requires two gates to be open at the same time: the account's per-intent-destinations setting, which the merchant controls in the dashboard, and the route's payout mode, which only Canopy moves to Dynamic.
Each closed gate has its own refusal message. See payout destinations for both of those messages, the closed set of chains Canopy can deliver to, the admission refusals, and how Canopy resolves a destination it was not told directly.
One active intent per payout destination
There is exactly one dedupe rule: at most one active intent may exist per (merchant account, payout namespace, chain, wallet, token). A create naming a wallet that already has an active intent with the same terms returns that intent with created: false and a fresh widgetToken -- the previously issued token stops authenticating. The same destination with different priceUnits or oneTime is a 409: see idempotency_key_reuse. EVM addresses compare case-insensitively; Solana addresses compare byte-exactly. A different chain or token is a separate destination.
A create with no destination fields always mints a new intent and a new deposit address. Any supplied destination field enables dedupe, with omitted fields resolved from account defaults. A repeatable intent stays active after a deposit; archive it before changing its terms.
A retry naming the same destination and terms returns the same intent but rotates its widget token. Save the returned intent ID and serialize creates for that destination. A create without destination fields always creates another intent, so do not blindly retry it after a timeout. There is no request idempotency key; merchantReference does not deduplicate requests.
What happens after you create one
A successful create returns intentId, inboxAddress and widgetToken. Funding the address, mounting checkout, and watching for settlement is the full sequence from here -- see the quickstart. What settles, and how you find out, is covered in webhooks.