Payout destinations
Name where a user's own deposit settles, on any of the chains Canopy delivers to, and know exactly what Canopy has and has not confirmed by the time you get a 201.
A payment intent can name its own settlement destination. Live settlements already pay caller-named addresses today.
Two independent gates must both be open before a destination-naming create is admitted: the merchant's own per-intent-destinations setting, which the merchant turns on and off in the dashboard, and the route's on-chain payout mode, which only Canopy can move to Dynamic. Read this page before you send payoutWallet, payoutNamespace, or payoutChainReference on a live create call.
Name a destination on an intent
POST /api/v1/intents accepts three optional fields for this: payoutWallet (1 to 64 characters), payoutNamespace (the enum "eip155" or "solana"), and payoutChainReference (1 to 64 characters). A destination counts as supplied when any one of the three is present.
Omitting payoutNamespace and payoutChainReference resolves both to the settle chain, so payoutWallet alone names that address on the settle chain. Supplying payoutNamespace or payoutChainReference without payoutWallet is refused with a named 400 decided in the create handler, not a schema-level rule.
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 destination is write-once. It is fixed when the intent is created, and the public API has no operation that updates an intent -- to settle to a different address, create another intent. An intent created while both gates below were open keeps its destination and still settles even if the merchant later turns their setting off.
The two gates
A create that supplies a destination is admitted only when both of these are true, checked in the order below. The two refusals carry deliberately distinct messages: the first names something the merchant can fix themselves, the second names something only Canopy can change.
| Gate | Who controls it | Refusal when closed |
|---|---|---|
| The account's per-intent-destinations setting, which is on by default | The merchant, in the dashboard under account → Payments | 400 Per-intent destinations are disabled for this account. Enable them in the dashboard. |
The settle-chain route's confirmedPayoutMode is Dynamic | Canopy, through the settler cold owner | 400 Route does not accept a per-intent destination |
The setting is checked first on purpose. A merchant whose switch is off is told about the switch, not about a route they cannot touch -- even when the route is also not Dynamic yet.
Treat the two messages as different conditions, never as one generic refusal: the first clears the moment the merchant turns the setting back on, the second clears only when Canopy opts the route in.
A create that carries no destination field never consults either gate. It settles to the merchant's account payout wallet exactly as it always has.
The account setting is self-service; the route's payout mode is not. No API call, request header, or dashboard control puts a route into Dynamic mode -- that is a Canopy-side action through the settler cold owner. If you see Route does not accept a per-intent destination, ask Canopy; there is nothing to change on your side.
Where Canopy can deliver
The admitted set is a closed list of exactly four namespace-and-reference pairs:
| Pair | Chain | Delivery |
|---|---|---|
eip155:4663 | Robinhood Chain (settle chain) | Direct -- no egress hop |
eip155:8453 | Base | Egress |
eip155:42161 | Arbitrum | Egress |
solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp | Solana | Egress |
A pair outside this list is refused with Destination chain is not a supported settlement destination. A wallet whose shape does not match its namespace is refused with Destination wallet is malformed for its namespace. Pair membership is checked before address shape, so a badly-formed address on an unsupported chain returns the chain refusal, never the address one.
Address rules by namespace: an eip155 address must be checksummed, and the zero address is refused. A solana address must base58-decode to exactly 32 bytes, and the all-zero address is refused.
Off the settle chain, delivery depends on the settlement worker holding a configured egress vault address. A worker with none refuses with egress_not_configured.
Cross-chain delivery is accepted and validated at create time. Whether it actually delivers depends on that worker configuration -- never guaranteed by the create response alone.
How Canopy picks the destination
Under Dynamic mode, the settlement worker resolves the destination from the deposit customer's active intents at settle time. Zero active intents is refused with no_intent_destination; more than one is refused with ambiguous_intent_destination.
Both are refusals, never a guess -- this is why an integration keeps at most one active intent per customer per destination. A Fixed-mode route never reads an intent at all.
What a 201 does not promise
The create-time confirmedPayoutMode check reads the settle chain's own state at create time, and it carries a known staleness window: the route's mode can change on-chain between that read and the deposit's eventual settlement. It is a hint, never the settle-time authority.
The settlement worker's own live on-chain payoutMode read inside prepare() is the sole authority at settle time. The account setting is read at create time only -- the settlement worker never consults it, which is why an intent already created keeps settling to its named destination after the setting is turned off.
A 201 means the intent was accepted. It does not mean a future settlement is guaranteed to reach the named address -- that is decided again, independently, when the deposit actually settles.
Replays stay predictable either way: a byte-identical replay of a create that already succeeded returns that same intent. A replay naming a different destination is refused with 409.