In order to send payouts, you will need to deposit funds to a liquidity provider to exchange fiat to stablecoins (i.e., on-ramp funds).
Tesser treats deposits via a liquidity provider as a special class of payments, so the structure of the deposits API experience will be similar to creating a payment.
Before creating a deposit, review the Funds Movement Lifecycle and Data Model overview to understand the shared data shape, lifecycle phases, and statuses that apply to deposits and Tesser's other funds-movement resources.
Registering Your Bank Accounts and Wallets with Liquidity Providers
Liquidity providers require that funds movement to and from their platforms are "first-party" only. So, your bank accounts (for fiat funds movement to/from the provider) and wallets (if using self-custodial wallets) must be registered with the liquidity provider prior to deposits and withdrawals.
Before beginning a deposit, you also must register the relevant bank account and/or wallet with Tesser. Where possible, Tesser will then register these accounts at your enrolled liquidity providers on your behalf. For more information on account creation, see Create an account.
Deposit Workflow
Like a payment, a deposit will execute over multiple steps.
A transfer step indicates funds are moving from one account to another. Accounts can be a bank account, ledger, or wallet. In a transfer step, the source and destination currencies (estimated.from.currency and estimated.to.currency) may be the same or different.
A swap step indicates currencies have been exchanged within the same account (estimated.from.account_id and estimated.to.account_id are the same). E.g., a trade was performed to sell USD and buy USDC or USDT within your ledger at a liquidity provider.
Each step has a status. For more information on the statuses of a transfer step, see Step statuses.
Exchange Rates for On-Ramping
Depending on which liquidity provider is being used, there may be variation in the exchange rate between a given fiat and stablecoin pair when you on-ramp. Also, the liquidity provider may or may not offer the ability to guarantee the current rate. At this time, Tesser does not provide guaranteed exchange rates unless the provider does.
Type of liquidity provider
Example
Behavior
Stablecoin issuer
Circle
Exchange rate between USD and USDC always 1:1
On/Off ramp
Alfred, OpenFX
Exchange rate between buy and sell currencies fluctuates; current rates can be guaranteed for a period of time
Exchange
Kraken
Exchange rate fluctuates; no guaranteed rate
Funding Deposits
Tesser allows you to create "set-it-and-forget-it" deposits for all liquidity providers, where the desired.from.currency and desired.to.currency are different. In this scenario, Tesser will supply an indicative exchange rate between the two currencies via the estimated.from.amount and estimated.to.amount fields, and once funds from the desired.from.currency land at the liquidity provider, Tesser will attempt to execute the on-ramp into the desired.to.currency at the current exchange rate.
Depending on the liquidity provider, you may be able to pre-position funds (pre-fund) at the provider. If pre-funding is supported, Tesser will create a ledger account at the provider to track your pre-funding levels (if any). For liquidity providers that support pre-funding, you can create deposits that transfer only fiat funds from your source bank account to the ledger of the liquidity provider.
You may want to pre-fund a liquidity provider if the provider offers guaranteed rates. Pre-funding at a liquidity provider that offers guaranteed rates can be beneficial because the exchange rate quoted by the liquidity provider can be directly executed because funds are already stored with the liquidity provider.
If you choose to pre-fund, you will create a deposit for the transfer of fiat to the provider and then a rebalance to trade from fiat into a stablecoin.
Deposit Creation
Submit a request to the Deposit API.
If applicable, you should specify on which tenant's behalf you are requesting the deposit.
For the deposit, populate the following fields in the desired object:
desired.from.account_id: The identifier of the bank account fiat funds will be sent from (see above).
desired.from.amount: Fiat amount to deposit.
desired.from.currency: Fiat currency you are on-ramping from.
desired.to.account_id: Identifier of the self-custodial wallet or provider ledger account to deposit stablecoins to.
Ensure the desired.to.account_id is associated with a liquidity provider that can support your requested desired.from.currency and desired.to.currency.
desired.to.currency: Stablecoin currency you are on-ramping to.
desired.to.network: Stablecoin network on-ramping to (optional). Not applicable if desired.to.account_id has a type of "ledger".
Note: desired.to.amount is not auto-populated. The Deposit's desired.to.amount remains null throughout the lifecycle; the indicative target amount is supplied in estimated.to.amount once the quote is obtained.
The examples in this guide show 3 scenarios:
Depositing and on-ramping to a ledger at Circle
Depositing fiat to a ledger at OpenFX
Depositing and on-ramping into a self-custodial wallet via OpenFX
Example request (deposit and on-ramp to a ledger at Circle)
After your POST /deposits request is accepted, Tesser plans the route the funds will take through the liquidity provider and obtains a reference exchange rate. Tesser then sends a deposit.quote_created webhook — the first webhook fired for the deposit. The payload carries the planned steps[] array (each step with status: "created") together with the populated estimated overlay at both the deposit and step levels. This webhook fires for all liquidity providers, including those with fixed 1:1 rates such as Circle, for consistency across the deposit lifecycle. The ratio of estimated.from.amount and estimated.to.amount is the indicative exchange rate for the deposit.
Example deposit.quote_created webhook (deposit and on-ramp to a ledger at Circle):
Obtain Deposit Instructions for Liquidity Provider
The estimated.to.account_id field in the first step will be populated with the Tesser identifier of the provider's bank account. Submit a request to Get an account by ID with the to_account_id as the path parameter.
The response will contain the bank account information you need to deposit funds to the liquidity provider, including the account number, bank identifier code (BIC/SWIFT/Routing number).
Once you have the account information, initiate a push of funds to the liquidity provider from your bank account. This funds transfer happens outside of Tesser's system. The instructions will include the supported payment methods you can use to push funds. For example, some liquidity providers will only accept wires.
Follow provider instructions carefully
Make sure to follow the liquidity provider's deposit instructions carefully. Failing to comply with the instructions can cause delays in deposits or deposits to be rejected.
Deposit Info After On-Ramping Completes
Tesser will track the receipt of the fiat funds to the liquidity provider, as well as any on-ramping into stablecoins and withdrawal to a self-custodial wallet (if applicable). When a deposit is successful, the status of each step will be "completed" and the actual.* fields will be populated on the top-level of the deposit and on each step. At the terminal state, Tesser emits two webhooks: a step.completed event for the last step, followed by a deposit.updated event carrying the full updated Deposit object with all overlays populated.
Example step.completed webhook (terminal step for deposit and on-ramp to a ledger at Circle):
Below are the webhook events you will observe for each of the three deposit scenarios in this guide.
Scenario 1 — Deposit and on-ramp to a ledger at Circle
#
What happens
What you receive / do
Fields populated
1
You submit the deposit request
HTTP response with id
desired.from.amount
2
Tesser plans the steps and obtains a quote
deposit.quote_created webhook fires with 2 steps and the populated estimated overlay
Steps array; estimated.from.amount and estimated.to.amount at deposit and step level (1:1)
3
You wire USD to Circle's bank account
—
—
4
Circle receives your wire, mints USDC, and credits your Circle ledger
Two terminal step.completed events fire (one per step), followed by a deposit.updated event carrying the full Deposit with actual.* populated. Tesser observes Circle's single terminal webhook, so no intermediate step.submitted or step.confirmed events are emitted. If the wire cannot be reconciled, two step.failed events fire instead, also followed by deposit.updated.
Steps 1 & 2: actual.from.amount, actual.to.amount, all timestamps. Deposit-level actual.from.amount and actual.to.amount. Deposit complete.
The two step.completed events for Steps 1 and 2 arrive in close succession, followed immediately by deposit.updated.
Scenario 2 — Deposit fiat to a ledger at OpenFX
#
What happens
What you receive / do
Fields populated
1
You submit the deposit request
HTTP response with id
desired.from.amount
2
Tesser plans the steps and obtains a quote
deposit.quote_created webhook fires with 2 steps and the populated estimated overlay
Steps array; estimated.from.amount and estimated.to.amount at deposit and step level (1:1)
3
You wire USD to OpenFX's bank account
—
—
4
OpenFX receives your wire and credits your OpenFX ledger
Two terminal step.completed events fire (one per step), followed by a deposit.updated event carrying the full Deposit with actual.* populated. Tesser only observes OpenFX's terminal reconciliation webhook, so no intermediate step.submitted or step.confirmed events are emitted. If the wire cannot be reconciled, two step.failed events fire instead, also followed by deposit.updated.
Steps 1 & 2: actual.from.amount, actual.to.amount, all timestamps. Deposit-level actual.from.amount and actual.to.amount. Deposit complete.
The two step.completed events for Steps 1 and 2 arrive in close succession, followed immediately by deposit.updated.
Scenario 3 — Deposit and on-ramp to a self-custodial wallet via OpenFX
#
What happens
What you receive / do
Fields populated
1
You submit the deposit request
HTTP response with id
desired.from.amount
2
Tesser plans the steps and obtains a quote
deposit.quote_created webhook fires with 4 steps and the populated estimated overlay
Steps array; estimated.from.amount and estimated.to.amount at deposit and step level
3
You initiate a push of funds to the liquidity provider's bank account
—
—
4
Funds arrive at the liquidity provider and are credited to its ledger
Two terminal step.completed events fire for the fiat-transfer steps (steps 1 and 2). Tesser only observes OpenFX's terminal reconciliation webhook, so no intermediate step.submitted or step.confirmed events are emitted for these steps.
Steps 1 & 2: actual.from.amount, actual.to.amount, all timestamps. Deposit-level actual.from.amount.
5
Tesser executes the on-ramp trade at the liquidity provider
Step lifecycle events fire for the swap step
Step 3: actual.from.amount, actual.to.amount (reflects actual fill rate), all timestamps
6
Withdrawal to your wallet is initiated
step.submitted on the wallet-transfer step
Step 4: submitted_at, actual.from.amount
7
The on-chain transaction is visible on the network
step.confirmed on the wallet-transfer step
Step 4: confirmed_at, transaction_hash
8
The transfer reaches finality; deposit is complete
step.completed on the wallet-transfer step, and a deposit.updated event fires with the terminal Deposit object
Lifecycle events for Steps 1, 2, and 3 will arrive in close succession.
Failure Modes for Deposits
If a deposit fails, the actual.* fields on the top-level of the deposit will represent the final account the funds are in, the final amount and currency in the final account, and on which blockchain network the funds are held (if applicable). For the steps that did successfully complete (if any), the actual.* fields will also be populated. The actual.to.* fields of the last successful step will match the actual.to.* fields at the top level of the deposit. You can inspect the status_reasons of the step where the failure occurred for more information about the cause of the failure. Any steps subsequent to the failure step will also have status = failed, with null actual.* fields.
Example: Trade Fails to Complete at an Exchange or Ramp
If Tesser attempts the on-ramp trade at the liquidity provider and cannot get the trade to succeed before the deposit's expires_at timestamp, the deposit terminates as a fiat-only deposit. This means the source fiat remains at the liquidity provider, but the conversion into the target stablecoin did not happen. This situation can occur in scenario 3 (OpenFX with on-ramp).
What you will observe:
step.failed on the swap step. actual.to.amount remains null because no stablecoin was obtained. The swap is step 3 in scenario 3.
step.failed on the subsequent wallet-transfer step. This step never entered submitted, because there were no stablecoins to withdraw. The wallet-transfer step is step 4 in scenario 3.
The deposit's top-level actual.to.currency and actual.to.amount are populated to reflect the final outcome — the deposit completed in the source fiat. desired.to.currency stays as the originally requested stablecoin (client intent is never overwritten). For a USD→USDT deposit, the terminal state has actual.to.currency: "USD" with actual.to.amount equal to actual.from.amount.
The fiat balance at the ledger with the liquidity provider is available to use for a rebalance.
A deposit.updated webhook fires alongside the terminal step.failed events, carrying the full updated Deposit object with the populated actual.* overlay. You can use this webhook instead of GETting the deposit to observe the terminal state.
The JSON examples below show steps 3 and 4 from scenario 3.
Example webhook schema for step.failed on step 3 (swap):
Example: Fiat Deposit Never Arrives or Is Not Credited
Tesser relies on the liquidity provider for notification of when your fiat funds have been received and credited. If that notification never arrives within the deposit's expires_at window — e.g., the wire was not sent, was returned, is stuck at an intermediary bank, or was received but could not be reconciled to your account at the liquidity provider — Tesser will time the deposit out. All steps that were still in created status will transition to failed, and step.failed events will fire for each. A deposit.updated webhook then fires with the full updated Deposit object reflecting the terminal state. The same pattern applies in scenarios 2 and 3 — all unstarted steps transition to failed and the deposit's top-level actual.* fields remain null because no funds ever moved.
The example below illustrates scenario 1 (Circle, two steps); the shape of the payload is the same in scenarios 2 and 3, with one failed step entry per never-started step.
Example deposit.updated webhook (Circle deposit, scenario 1, fiat never landed):