How to integrate multi-network deposits into your app
Connect the funds your user holds to the destination your app needs, with a deposit flow you can track and recover.
Your app may need funds in one asset on one network, while a user holds a different asset somewhere else. A deposit integration has to connect those two positions and keep an accurate record of what happened along the way.
Canopy is a universal deposit system for apps. Your backend creates a deposit intent, your user funds an address on a supported network, and Canopy handles detection, routing and settlement. Funding assets can include native assets; the asset delivered can differ from the asset the user sent.
This guide covers the integration decisions that matter most: identifying the user, selecting a destination, showing a valid funding option and recording settlement on your server.
Start with the user and destination
Create an intent when you know which user is about to deposit. Store its intentId against that user's record in your own database. A reference such as merchantReference helps reconciliation, but it does not make repeated create requests idempotent.
An intent can specify payoutWallet, payoutNamespace, payoutChainReference and payoutTokenAddress. Those fields describe the destination, including its asset. They do not describe the network from which the user will send funds.
Resolve that destination on your server. Validate that the user is entitled to fund the account you associate with it. Once created, the intent's destination is pinned; changing the destination requires another intent. Available destination assets depend on configured live chains and assets, together with the account's provisioned routing.
For a first integration using your account defaults, the server request is small:
const response = await fetch("https://www.canopypay.io/api/v1/intents", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CANOPY_SECRET_KEY}`,
"Canopy-Version": "2026-09-01",
"Content-Type": "application/json",
},
body: JSON.stringify({ merchantReference: depositReference }),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}
const { intentId, inboxAddress } = await response.json();Here, depositReference is an identifier your application supplies. Save the returned intent before showing the deposit flow. Keep the secret key on your server. The quickstart covers mounting the supplied widget with the resulting intent ID.
Offer funding options from the current configuration
A network being supported in general does not mean every asset or deposit path is available for every intent. Read the options for the actual intent instead of maintaining a hardcoded network and token list.
If you use Canopy's widget, it resolves the funding flow. For your own interface, use the source inbox API:
- Call
GET /api/v1/intents/{id}/originsfrom your server. - Use the returned network, token and minimum fields to describe each funding option. Inspect the intent-level
sourceDepositIssuableflag and each origin'sdepositIssuableflag. - Call
POST /api/v1/intents/{id}/source-inboxfor the chosen origin. Use the returnedaddressandcaip10to associate the address with its network.
Keep the network and asset visible beside the deposit address. The presence of an address alone does not establish that every network or token is valid for it. When an option is temporarily unavailable, explain the condition in the interface instead of presenting an address as ready to fund.
Make retries deliberate
Two create requests can have different consequences. A create with no destination fields creates a new intent and address. Blindly repeating that request after a timeout can leave your app tracking more than one intent for the same deposit attempt.
A create specifying a destination deduplicates against the active intent for that account, destination network, wallet and token. Matching terms return the existing intent with created: false and a fresh widget token; the previous widget token stops authenticating. Different price or one-time terms for the same active destination produce a conflict.
Serialize creates for the same destination and keep the returned intent ID. Decide how your application recovers an uncertain response before adding automatic retries. The intent contract explains these cases.
Source inbox issuance is different: repeating issuance for the same customer and origin returns the existing address. Even there, inspect the error code. source_inbox_not_active is a nonretryable refusal despite its HTTP 503 status; customer_not_bound and source_inbox_unavailable can be retried after Retry-After.
Separate a browser update from a server record
A successful intent creation means the intent was accepted. It does not mean the user has deposited. Likewise, the widget's paid event is a user-interface signal; it is not authority to credit an account.
Process verified settlement information on your server. Canopy signs webhook deliveries using Standard Webhooks:
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(endpointSecret);
const payload = webhook.verify(rawBody, headers);Use the original request body and the received webhook headers. Verify before acting, check the payload's state, and reject intent IDs that your backend did not create. Persist the event before returning HTTP 200. Deduplicate using webhook-id, which remains stable across retries, so a repeated delivery cannot credit the user twice.
Also define the milestone your app needs: deposit detection, settlement and arrival at a cross-network destination are distinct steps. Confirm the required destination-delivery behavior for your route before treating an earlier milestone as final receipt. The webhook documentation covers the signed settlement payload.
Test the interruptions users will encounter
Before opening deposits to users, exercise a successful flow and the interruptions around it: a timed-out create, a reused destination, an unavailable funding origin, an amount below the displayed minimum, a closed browser and a repeated webhook delivery. Keep intent IDs and API error request_id values in access-controlled operational logs.
An integration is ready when your app can explain what happened and recover without inventing a second deposit or crediting the same event twice. Start with Canopy's quickstart, then implement the recovery paths alongside the successful path.