This guide is for payout creation when funds are held in self-custodial wallets.
Before creating a payout, review the Funds Movement Lifecycle and Data Model overview to understand the shared data shape, lifecycle phases, and statuses that apply to payouts and Tesser's other funds-movement resources.
To create a payout, send a POST request to the Payments API.
For payouts, you can supply all the required information in the creation API call, or only supply the minimally necessary information for payment creation and then update the payment with account information. If you take the two-step approach, Tesser publishes an additional payment.updated webhook after the PATCH succeeds — see Update payout with account information.
Payouts expire
Every payout has an expires_at timestamp. If a payout doesn't complete by then — for example, the wallet's balance never reaches reserved because funds weren't replenished, or signing wasn't submitted in time — it cannot be resurrected. To retry, create a new payout. See Expiration for details.
Payout creation
At a minimum, supply the following information to create the payout record in the desired object:
Currencies: desired.from.currency and desired.to.currency.
Amounts: Either desired.from.amount or desired.to.amount (not both).
Networks: For stablecoin payouts, specify both desired.from.network and desired.to.network. For fiat payouts, the receiving network is null.
You may also supply desired.from.account_id, desired.to.account_id, and funding_account_id at this stage, or omit them and PATCH them later — see Update payout with account information.
Only one of desired.from.amount or desired.to.amount may be specified in the request, never both.
In the synchronous response, you receive an id for the payout that you can use for subsequent PATCH or GET calls or to track webhook updates received for this payout. The desired.* overlay echoes what you submitted; estimated.* and actual.* are populated as the payout progresses.
After you create the payout, Tesser asynchronously plans the route — sourcing the best exchange rate (if cross-currency) and computing the sequence of steps[] needed to deliver funds. Both pieces are emitted on a single payment.quote_created webhook. See Planning on the lifecycle overview for the full picture.
The webhook populates:
The top-level estimated.* overlay. For same-currency moves, estimated.* matches desired.* 1:1. For cross-currency moves, the ratio of estimated.from.amount to estimated.to.amount is the indicative exchange rate.
The steps[] array. Each step starts with status: "created" and its own estimated.{from,to} overlay; actual.{from,to} is null until the step executes.
If you created the payout without desired.*.account_id fields, those fields stay null on the payment.quote_created payload too. Customer-side account_id fields on the steps — estimated.from.account_id on step 1 (the source wallet) and estimated.to.account_id on the last step (the final beneficiary) — also stay null until you PATCH accounts and Tesser re-plans (see next section). Provider-ledger account ids on intermediate step boundaries may already be populated.
Example payment.quote_created webhook for a stablecoin payout (same-currency, single on-chain step):
If you did not supply account information at creation, submit a PATCH request to the Payments API to populate:
desired.from.account_id: Wallet on the Tesser platform that funds will come from. Because this guide is about wallet payouts, ensure this is a managed wallet (is_managed = true).
desired.to.account_id: Wallet or bank account of the beneficiary that funds will be delivered to.
funding_account_id: Fiat bank account of the ultimate originator of the payout.
Because the Tesser platform serves financial institutions, the desired.from.account_id may belong to a counterparty, tenant, or the workspace. funding_account_id is therefore required to determine the ultimate originating entity of the payout to fulfill Travel Rule obligations. See Travel Rule.
Example PATCH request for stablecoin payout updated with accounts:
After the PATCH succeeds, Tesser publishes a payment.updated webhook reflecting the now-complete desired.* overlay. This is unique to payouts — deposits, withdrawals, and rebalances require all desired.* fields at creation, so they don't get this mid-lifecycle event. (A second payment.updated fires later when the payout reaches its terminal state — see Payout terminal state.)
Tesser will also re-plan the route now that account ids are known: step-level account_id fields are populated and provider_key may be set on each step. If payment.quote_created already fired during creation with null account_ids, Tesser's payment.updated after PATCH carries the re-planned steps[].
Example payment.updated webhook after PATCHing accounts on a stablecoin payout:
Once you supply desired.to.account_id, Tesser screens the beneficiary's wallet (and any intermediate wallets in the route) against your organization's risk policy. The outcome is reported on the payment as risk_status via a payment.risk_updated webhook. See Risk Statuses on the lifecycle overview for the full taxonomy.
If your policy requires manual review, submit a decision via the risk review decision API or in the Tesser dashboard. After a decision is recorded, risk_reviewed_by and risk_reviewed_at are populated and risk_status transitions to manually_approved or manually_rejected.
Payment step signing
Once a payout has been approved via risk review, Tesser will send a webhook with event type step.signature_requested with the necessary information to sign the on-chain payment step.
Ensure you proceed with step signing prior to the expiration time, as indicated by the payout's expires_at timestamp. Otherwise, you will need to create a new payment.
Example webhook schema for stablecoin payout requesting signature:
To sign the step, use the LocalSigner SDK to construct and sign the transaction locally. The signature is returned as a string, which you then submit to the Sign payment step API to execute the payment.
Wallet and recipient addresses are automatically resolved from the payment's account IDs; you only need to pass the payment object.
Code
import { LocalSigner, TesserApi } from "@tesser-payments/sdk";// Initialize API clientconst client = new TesserApi({ // Get a valid token following the authentication process // Additional details: https://docs.tesser.xyz/overviews/authentication#generate-an-api-token token,});// Initialize the signer (once at application startup)const signer = new LocalSigner(client, { publicKey: process.env.SIGNING_PUBLIC_KEY, privateKey: process.env.SIGNING_PRIVATE_KEY, enclaveId: process.env.SIGNING_ENCLAVE_ID,});// 1. Extract the step from a step.signature_requested webhook eventconst step = webhookPayload.data.object;// 2. Sign the stepconst result = await signer.signStep(step);// 3. Submit the signature to execute the stepawait fetch( `https://api.tesser.xyz/v1/payments/${step.transfer_id}/steps/${step.id}/sign`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ signature: result.signature }), });
unsignedTransaction: the serialized unsigned transaction
metadata: full signature details (stampHeaderName, stampHeaderValue, body)
Balance check
When you submit the signature for a wallet step, Tesser also checks the balance of the wallet at desired.from.account_id. For the full status taxonomy, see Balance Statuses on the lifecycle overview.
If there are sufficient funds in the wallet, you receive a synchronous success (200) from the Sign payment step API and two webhooks fire:
payment.balance_updated with balance_status: "reserved" and balance_reserved_at populated.
step.signed confirming the on-chain step was successfully signed; status becomes "signed".
Example webhook schema for stablecoin payout after successful balance check:
If the wallet has insufficient funds, the synchronous response is a 4XX with an error code, and payment.balance_updated fires with balance_status: "awaiting_funds". The payout is queued — Tesser will republish step.signature_requested (and refresh signature_requested_at) once the wallet is funded, until expires_at. Repeat the signing flow with the freshly published step to retry.
Example webhook schema for stablecoin payout after failed balance check:
The payment will remain queued, with balance_status = "awaiting_funds" until you have added sufficient funds to the wallet or the payment expires, whichever occurs first. If additional funds are added to the wallet prior to the payment expiration time, Tesser will republish the step.signature_requested webhook and update the signature_requested_at timestamp. You will then repeat the signing action and submit the new signature to the Sign payment step API.
On-chain processing
After successful signing, Tesser broadcasts the payment on-chain. The step's status transitions through submitted → confirmed (each emits a step.* webhook — see Execution). The blockchain transaction_hash is populated on the step once submitted, and gas fees appear in the per-step fees[] array once confirmed.
Tesser sponsors gas fees for your organization, so on-chain transfers will not fail due to insufficient native-token balance in the wallet. Your organization is billed at the end of the month for accrued gas fees.
Example webhook schema for stablecoin payout after blockchain network confirmation:
Note: at step.confirmed for a successful step, the per-step actual.{from,to} becomes populated to match estimated.{from,to} (this is the first event that populates step-level actual.*).
Fiat step processing
Once the stablecoin funds reach the off-ramp provider, Tesser instructs the provider to deliver fiat to the beneficiary at desired.to.account_id. The provider's last-mile transfer appears as the final step in the route and progresses through the same step-status path — see Step Statuses.
Payout terminal state
When the last step reaches completed, Tesser populates the top-level actual.* overlay and emits a payment.updated webhook carrying the full updated payment object. Use this event to observe the terminal outcome without polling. See Terminal State and Divergence for the full picture, including how actual.* may diverge from desired.* on failure.
For payouts that used the two-step create-then-PATCH pattern, this is the secondpayment.updated event you receive — the first fired after the PATCH supplied account ids (see Update payout with account information).
Example terminal-state payment.updated webhook for a successful stablecoin payout: