Guides
Source deposit inboxes
Your user holds funds on a chain other than the settle chain, and you want a deposit address for that chain without mounting the widget.
When you need this
GET /api/v1/intents/{id}/origins and POST /api/v1/intents/{id}/source-inbox are a headless, server-to-server pair: no widget, and no customer-session cookie. If your integration already mounts the checkout widget, it never needs to call either of these -- see the quickstart for the widget path instead.
List the origins you can offer
GET /api/v1/intents/{id}/origins returns every live source origin for the intent's account, each carrying its own depositIssuable flag, alongside settleRails and an intent-level sourceDepositIssuable flag. Origins are returned even when sourceDepositIssuable is false, so a caller renders the full list and explains the refusal instead of showing an empty picker.
This determines how you build your picker: never hide the list because the intent-level flag is false.
The route returns 404 both for an archived intent and for another merchant's intent, and the two are never distinguished.
curl https://www.canopypay.io/api/v1/intents/6f1b9c2e4d8a4f1b8c2e4d8a4f1b8c2e/origins \
-H "Authorization: Bearer cnpy_sk_live_a1b2c3_<64 hex chars>" \
-H "Canopy-Version: 2026-09-01"{
"origins": [
{
"namespace": "eip155",
"reference": "8453",
"chainId": 8453,
"name": "Base",
"shortName": "Base",
"publicRpcUrl": "https://mainnet.base.org",
"explorerUrl": "https://basescan.org",
"nativeName": "Ether",
"nativeSymbol": "ETH",
"nativeDecimals": 18,
"token": {
"symbol": "USDC",
"address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"decimals": 6
},
"caip19": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"minimumUnits": "2082500",
"minimumDisplay": "2.0825",
"depositIssuable": true
}
],
"settleRails": [
{
"id": "robinhood-mainnet:usdg",
"chainId": 4663,
"chainName": "Robinhood Chain",
"token": {
"symbol": "USDG",
"address": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
"decimals": 6
},
"explorerUrl": "https://robinhoodchain.blockscout.com",
"status": "live",
"default": true
}
],
"sourceDepositIssuable": true
}Issue an inbox for one origin
POST /api/v1/intents/{id}/source-inbox issues, or returns, the deposit inbox address for one source origin on the intent's bound customer. The body has two arms: { chainId } for an EVM origin, or { namespace, reference, mint } for Solana.
A repeat call returns the same address and never a second one, so the call is safe to retry. The 201 response carries address and caip10.
curl -X POST https://www.canopypay.io/api/v1/intents/6f1b9c2e4d8a4f1b8c2e4d8a4f1b8c2e/source-inbox \
-H "Authorization: Bearer cnpy_sk_live_a1b2c3_<64 hex chars>" \
-H "Canopy-Version: 2026-09-01" \
-H "Content-Type: application/json" \
-d '{ "chainId": 8453 }'{
"address": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
"caip10": "eip155:8453:0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
}When issuance is refused
Two of the four refusals are 503 and retryable, and two are not, and the split does not follow the status code.
source_inbox_not_active is a 503 that is not retryable. A caller that retries every 503 will hammer a refusal that will never clear -- check the code, not the status alone.
| Code | Status | Retry | What to do |
|---|---|---|---|
| source_inbox_unavailable | 503 | Yes, after Retry-After | The gate is temporarily unreachable |
| source_inbox_refused | 500 | No | Contact Canopy support |
| source_inbox_not_active | 503 | No | Issuance is disabled for this origin; offer a different one |
| customer_not_bound | 503 | Yes, after Retry-After | The intent's deposit customer is not bound yet |
See also
- Quickstart: hands your user a deposit address the widget way, at its hand-your-user-a-deposit-address step.
- Payment intents: the full create-body contract this guide's intent already exists under.
- API reference: both operations, with their full request and response shapes.