Forwarding Address Integration Guide
Typical Integration Flow
See the API Reference for details on each method.
Send every request to https://api.candide.dev/forwarder/v3/<team-api-key>. The activation call (account_activateForwardingAddress) also requires Authorization: Bearer <token>, using the bearer token of a forwarder policy. Both come from the Candide Dashboard.
Getting Access
Forwarding Address access is self-serve through the Candide Dashboard:
- Create a team API key under API Keys. It goes in the forwarder endpoint URL.
- Under Forwarder Policies, create a policy. Name it and pick the source networks it accepts deposits on.
- Configure sponsorship and its limits (see below).
- Generate the policy's bearer token and store it in your server's secrets. It is shown only once and can only be rotated, not retrieved.
A forwarder policy controls the addresses activated with its bearer token:
- Source networks: the chains where deposits arrive and are forwarded. Deposits arriving on a removed chain are held on the address until the chain is added back.
- Active: pausing a policy holds all deposits on its addresses until it is re-enabled.
- Sponsorship: when on, the policy pays the forwarding fees from its balance, within its limits. Deposits it cannot pay for are held. When off, fees are deducted from each deposit and the policy's balance and limits are not used.
- Sponsorship limits: the maximum fee per forward, per address per UTC day, and per address over its lifetime, plus a rate limit per address.
The policy page also lists every address activated under it, with its status, forwards count, and fees sponsored.
Keeping Addresses Alive
Monitoring is TTL-based. When the activation expires, the relayer stops watching for deposits. Before presenting a forwarding address to a user, check its activation status and call account_activateForwardingAddress again if the TTL has expired.
account_activateForwardingAddress is idempotent. Calling it again resets the TTL. If a deposit arrives after expiration, the funds sit in the forwarding address until either:
- The address is reactivated via
account_activateForwardingAddress, at which point the relayer picks up the deposit - The funds are recovered via the recovery frontend
Multiple Addresses per Recipient
Use the optional salt parameter in forwarding_getAddress and account_activateForwardingAddress to generate distinct forwarding addresses for the same recipient and destination pair. Each unique salt produces a different deterministic address. This is useful for tracking individual deposits or creating per-transaction deposit addresses.
Active forwarding addresses are capped per account; refreshing an existing address does not count against the cap. If you need many short-lived per-deposit addresses, plan for the cap or contact Candide.
Tracking Deposit Status
Poll forwarding_getForwardsByRecipient with the recipient and destinationChainId to see forwards as they're picked up and delivered. Each entry is a self-describing Forward with a normalized status of pending, delivered, or failed — you don't need to branch on which bridge (Across, LayerZero, CCTP, or same-chain) handled the deposit.
If you already have the source transaction hash (e.g. from watching the user's wallet), forwarding_getForwardsByTx returns every forward that transaction produced without a recipient filter. Once you have a forwardId from either method, poll forwarding_getForwardById instead of refetching the full list.
There is still no webhook for deposit completion. Polling the destination chain balance directly remains a valid fallback, but the Forward methods give you delivery status, failure reasons, and bridge-specific tracking data (Across deposit ID, LayerZero GUID, CCTP nonce) without needing to know each provider's API.
The custodialWithdrawer Role
The custodialWithdrawer is a company-controlled secure wallet that acts as a safety net for stuck funds. If funds get stuck in a forwarding address (e.g. the relayer did not process them), both the recipient and the custodialWithdrawer can withdraw directly from the deployed contract on the source chain. The recipient can always withdraw immediately. The custodialWithdrawer can withdraw after a timelock.
This matters for every integration, not just custodial ones. If a user funds their forwarding address from an exchange or a wallet they don't control on the source chain, they may not be able to send a withdrawal transaction on that chain. Without a separate custodialWithdrawer, those funds are stuck permanently.
| Scenario | recipient | custodialWithdrawer | Outcome |
|---|---|---|---|
| Wallet or dapp (recommended) | User's address | Company's secure wallet | Company can recover stuck funds on behalf of the user after a timelock. User can always withdraw immediately |
| Fully self-managed | User's address | Same as recipient | Only the user can recover stuck funds. If they cannot transact on the source chain, funds are unrecoverable |
| Custodial (exchanges, neobanks) | End-user's address | Company's secure wallet | Company can withdraw stuck funds on behalf of the user after a timelock |
Setting custodialWithdrawer to the same address as recipient means only the recipient can recover stuck funds. If users fund the forwarding address from an exchange or any source they don't control, and funds get stuck, there is no fallback recovery path. For most integrations, set custodialWithdrawer to your company's secure wallet.
Withdrawal priority
- The
recipientcan always withdraw immediately from the deployed contract (no timelock) - The
custodialWithdrawercan withdraw after a timelock period, providing a recovery path when the recipient cannot transact on the source chain
Fund recovery
If funds are stuck, use the recovery frontend to recover them.
Gotchas
- Deposits below the per-bridge minimum are not forwarded. Query
forwarding_getMinimumAmountfor the route and token to validate user input, and display the minimum clearly in your UI. - Only tokens listed in the route's
tokensarray are forwarded. Unsupported tokens sent to the address require manual recovery. - The forwarding address accepts deposits on any supported chain, including the destination chain. Do not send on unsupported chains. Check
forwarding_getRoutesfor the full list of supported chains per route. - The output token on the destination chain may have different decimals than the input token. Use the token's
decimalsfromforwarding_getRouteswhen formatting amounts. - Cache
forwarding_getRouteson app load and refresh periodically. Routes, fees, and supported tokens can change. - The activation call needs a forwarder policy's bearer token (
Authorization: Bearer ...). Keep it on your server: anyone holding it can activate addresses that spend your policy's balance. Exceeding the active address cap returns error-32013. - With sponsorship on, deposits are held (not forwarded) when the policy's balance or limits cannot cover the fee. Watch the policy balance in the Dashboard.