CanopyAPI docs

How to integrate multi-network deposits into your app

Move funds from wherever your user holds them to where your app needs them, with a deposit flow you can track and recover.

Your app may need funds in one asset on one network. Your user may hold a different asset on another network. A deposit integration connects the two and keeps an accurate record of what happened.

Canopy handles deposits for apps. Your server creates a deposit intent. Your user funds an address on a supported network. Canopy detects the deposit, routes it and settles it. Users can fund with native assets as well as tokens. The asset that arrives can differ from the asset the user sent.

This guide covers the decisions that matter most: identifying the user, choosing a destination, showing valid funding options, and recording settlement on your server.

Start with the user and destination

Create an intent when you know which user is about to deposit. Save its intentId against that user in your own database. A merchantReference helps with reconciliation. It does not stop a repeated create request from creating a second intent.

An intent can set payoutWallet, payoutNamespace, payoutChainReference and payoutTokenAddress. These fields describe where the funds go, including the asset. They do not set the network the user sends from.

Work out the destination on your server. Check that the user is allowed to fund the account you link it to. After you create the intent, its destination is fixed. To use a different destination, create another intent. Which destination assets you can use depends on which chains and assets are live and on the routing set up for your account.

For a first integration that uses your account defaults, the server request is small:

create-deposit-intent.tstypescript
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 from your own app. Save the returned intent before you show the deposit flow. Keep the secret key on your server. The quickstart shows how to mount the widget with the returned intent ID.

Show the funding options for each intent

A network being supported does not mean every asset or deposit path works for every intent. Read the options for the actual intent. Do not hardcode a list of networks and tokens.

If you use Canopy's widget, it handles this for you. To build your own interface, use the deposit address API. The API calls a deposit address a source inbox.

  1. Call GET /api/v1/intents/{id}/origins from your server.
  2. Use the returned network, token and minimum fields to describe each funding option. Check the intent-level sourceDepositIssuable flag and each origin's depositIssuable flag. They tell you whether an address can be issued.
  3. Call POST /api/v1/intents/{id}/source-inbox for the option the user picks. Use the returned address and caip10 to tie the address to its network.

Show the network and asset next to the deposit address. An address on its own does not mean every network or token can be sent to it. If an option is temporarily unavailable, say so in your interface. Do not show its address as ready to fund.

Plan your retries

Two kinds of create request behave differently when repeated:

  • A create with no destination fields always makes a new intent and address. If you repeat it blindly after a timeout, your app can end up tracking more than one intent for the same deposit.
  • A create with a destination is matched against the active intent for that account, destination network, wallet and token. If the terms match, you get the existing intent back with created: false and a fresh widget token. The previous widget token stops working. If the price or one-time terms differ for the same active destination, you get a conflict.

Send one create at a time for each destination, and keep the intent ID it returns. Decide how your app recovers from an uncertain response before you add automatic retries. The intent contract explains these cases.

Issuing a deposit address works differently. Repeating it for the same customer and origin returns the same address. Still check the error code:

  • source_inbox_not_active: do not retry, even though the status is HTTP 503.
  • customer_not_bound and source_inbox_unavailable: retry after Retry-After.

Credit users from your server, not the browser

A successful create means Canopy accepted the intent. It does not mean the user has deposited. The widget's paid event is also only an interface signal. Do not use it to credit an account.

Credit the user from verified settlement data on your server. Canopy signs webhook deliveries with Standard Webhooks:

verify-webhook.tstypescript
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(endpointSecret);
const payload = webhook.verify(rawBody, headers);

Pass the raw request body and the webhook headers you received. Then:

  • verify the delivery before you act on it;
  • check the payload's state;
  • reject intent IDs your server did not create;
  • save the event before you return HTTP 200;
  • deduplicate on webhook-id. It stays the same across retries, so a repeated delivery cannot credit the user twice.

Decide which milestone your app treats as final. Deposit detected, settled, and arrived at a destination on another network are separate steps. Check how delivery works for your route before you treat an earlier step as final receipt. The webhook documentation covers the signed settlement payload.

Test the interruptions users will hit

Before you open deposits to users, test a successful flow and the interruptions around it:

  • a create request that times out;
  • a reused destination;
  • a funding origin that is unavailable;
  • an amount below the displayed minimum;
  • a browser closed partway through;
  • a webhook delivered more than once.

Keep intent IDs and API error request_id values in logs that only your team can access.

Your integration is ready when your app can explain what happened and recover without creating a second deposit or crediting the same event twice. Start with Canopy's quickstart, then build the recovery paths alongside the successful path.