# Tesser Documentation > Complete documentation for Large Language Models --- ## Document: Treasury Updates Webhook events fired during the deposit, withdrawal, and rebalance lifecycle URL: /webhooks/treasury-updates # Treasury Updates ## Deposit Events | Event | Fired when | | --- | --- | | `deposit.quote_created` | Route planning is complete; execution steps are created with exchange rate info | | `deposit.updated` | General notification that a field on the deposit has been updated | | `deposit.expired` | The deposit reached its expiration date-time and is no longer actively processing | ## Withdrawal Events | Event | Fired when | | --- | --- | | `withdrawal.quote_created` | Route planning is complete; execution steps are created with exchange rate info | | `withdrawal.balance_updated` | Balance check result is available (`reserved` or `awaiting_funds`) | | `withdrawal.updated` | General notification that a field on the withdrawal has been updated | | `withdrawal.expired` | The withdrawal reached its expiration date-time and is no longer actively processing | ## Rebalance Events | Event | Fired when | | --- | --- | | `rebalance.quote_created` | Route planning is complete; execution steps are created with exchange rate info | | `rebalance.balance_updated` | Balance check result is available (`reserved` or `awaiting_funds`) | | `rebalance.updated` | General notification that a field on the rebalance has been updated | | `rebalance.expired` | The rebalance reached its expiration date-time and is no longer actively processing | ## Step Events All other lifecycle updates for a deposit, withdrawal, or rebalance fire at the step level as each step progresses: | Event | Fired when | | --- | --- | | `step.signature_requested` | Signature has been requested for the step | | `step.signed` | Step has been cryptographically signed | | `step.submitted` | Step has been submitted for execution | | `step.confirmed` | Step execution is confirmed on-chain or by the partner | | `step.completed` | Step finished successfully | | `step.failed` | Step encountered an error and could not complete | | `step.updated` | General notification that a field on the step has been updated | The `data.object` for step events is the same across all Treasury resources. See for example the [DepositStep](/api/~schemas#deposit-step) resource. ## Payload The `data.object` in each to-level Treasury event contains the full resource. For the complete field reference, see the [Deposit schema](/api/~schemas#Deposit), [Withdrawal schema](/api/~schemas#Withdrawal), and [Rebalance schema](/api/~schemas#rebalance). --- ## Document: Payment Updates Webhook events fired during the payment lifecycle URL: /webhooks/payment-updates # Payment Updates ## Event Types | Event | Fired when | | --- | --- | | `payment.created` | A payment has been created in Tesser's system | | `payment.quote_created` | Route planning is complete; execution steps are created with exchange rate info | | `payment.balance_updated` | Balance check result is available (`reserved` or `awaiting_funds`) | | `payment.risk_updated` | Risk status has changed (e.g. `auto_approved`, `awaiting_decision`, or `auto_rejected`) | | `payment.updated` | General notification that a field on the payment has been updated. | | `payment.expired` | The payment reached its expiration date-time and is no longer actively processing | For a detailed breakdown of what each event means in the context of a payout, see [Payment Workflow](/overviews/funds-movement-lifecycle-and-data-model). ## Step Events Once steps are created (`payment.quote_created`), individual step lifecycle events fire as each step progresses: | Event | Fired when | | --- | --- | | `step.signature_requested` | Signature has been requested for the step | | `step.signed` | Step has been cryptographically signed | | `step.submitted` | Step has been submitted for execution | | `step.confirmed` | Step execution is confirmed on-chain or by the partner | | `step.completed` | Step finished successfully | | `step.failed` | Step encountered an error and could not complete | | `step.updated` | General notification that a field on the step has been updated | The `data.object` for Payment step events is a [PaymentStep](/api/~schemas#payment-step) resource. ## Payload The `data.object` for each top-level Payment event contains the full payment resource, including its steps. For the complete field reference, see the [Payment schema](/api/~schemas#Payment). For a walkthrough of how these events appear during a real payout lifecycle, see [Create a Payout](/how-tos/send-a-stablecoin-payout/create-a-stablecoin-payout). ## Key Status Fields For details on how `risk_status` and `balance_status` evolve through a payout, see [Payout Workflow](/overviews/funds-movement-lifecycle-and-data-model). --- ## Document: General Common envelope structure shared by all Tesser webhook events URL: /webhooks/general # General All Tesser webhook events share a common envelope structure. The envelope wraps event-specific data in a consistent format so your handler can route and process events uniformly. ## Envelope Fields | Field | Type | Description | | --- | --- | --- | | `id` | string | Unique event identifier | | `type` | string | Event type in `scope.action` format (e.g. `payment.quote_created`) | | `created_at` | string | ISO 8601 timestamp of when the event was created | | `data` | object | Contains the event-specific payload under `data.object` | ## Event Type Format Event types follow the pattern **`scope.action`**. Current scopes are `payment`, `deposit`, `withdrawal`, `rebalance`, and `step`. See [Payment Updates](./payment-updates) and [Treasury Updates](./treasury-updates) for event details. ## Example Payload for a Payment event ```json { "id": "90ee0a64-e8e9-43c0-8a60-20cf0cecf58f", "type": "payment.quote_created", "created_at": "2025-12-01T09:00:00.045Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "from_account_id": null, "funding_account_id": null, "to_account_id": null, "from_amount": "55.85", "from_currency": "USDC", "from_network": null, "to_amount": "1000", "to_currency": "MXN", "to_network": null, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.040Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` --- ## Document: Webhook Authentication Verify that webhooks originate from Tesser using Ed25519 signatures URL: /webhooks/authentication # Webhook Authentication Tesser signs every outgoing webhook request with an **Ed25519** asymmetric signature. You should verify this signature on your server before processing the payload. ## Signature Header Each webhook request includes the following headers: | Header | Description | | --- | --- | | `X-Tesser-Signature` | Base64-encoded Ed25519 signature of the request body | | `Content-Type` | Always `application/json` | | `User-Agent` | `Tesser-Webhooks/1.0` | The signature is computed over the exact UTF-8 bytes of the JSON request body. Do not parse and re-serialize the body before verifying — use the raw bytes. ## Public Key Tesser's webhook public key is provided in **SPKI DER** format, base64-encoded. You can also import it from the SDK types package: ```ts import { WEBHOOK_PUBLIC_KEY } from "@tesser-payments/types"; ``` ## Verifying Signatures ```js import { createPublicKey, verify } from "node:crypto"; import { WEBHOOK_PUBLIC_KEY } from "@tesser-payments/types"; function verifyWebhook(rawBody, signature) { const publicKeyObj = createPublicKey({ key: Buffer.from(WEBHOOK_PUBLIC_KEY, "base64"), type: "spki", format: "der", }); return verify( null, Buffer.from(rawBody, "utf8"), publicKeyObj, Buffer.from(signature, "base64"), ); } // In your webhook handler: const signature = req.headers["x-tesser-signature"]; const isValid = verifyWebhook(req.rawBody, signature); if (!isValid) { return res.status(401).json({ error: "Invalid signature" }); } ``` ## Important Notes - Always verify using the **raw request body** bytes. Parsing the JSON and re-serializing may change whitespace or key order, which will invalidate the signature. - If verification fails, respond with `401` and do not process the event. - The public key may be rotated in the future. Key rotation will be announced in advance and communicated through the Tesser Dashboard. --- ## Document: Treasury Management Stablecoin balance and yield management URL: /overviews/treasury-management # Treasury Management Tesser provides comprehensive treasury management capabilities for stablecoins. > Note: Most treasury management features are currently under development. Contact us to learn more about our roadmap and early access programs. **Balance Management** Effective balance management ensures payments can be processed successfully: - **Balance checks:** Before payment processing, the system validates that the source treasury wallet has sufficient funds to cover the payment amount and associated network fees and processing costs - **Balance reconciliation:** Regular balance verification for wallets against blockchain state - **Balance monitoring:** Low balance alerts when balances fall below thresholds or based on typical usage - **Automatic rebalancing**: Rebalance across tokens and chains to maintain optimal distribution **Yield Generation** Generate yield on otherwise idle stablecoin balances: - **Multiple Providers**: Integrations with yield providers to offer a range of options - **Risk Spectrum**: From US treasury-backed yield (lower risk) to DeFi yield options (higher yield, higher risk) - **Policy Controls**: Enable yield only where allowed by your policy and risk appetite - **Operational Safety**: Emphasis on capital preservation and liquidity availability for payments **Treasury Analytics** Comprehensive reporting and analytics: - **Utilization Reports**: Track fund usage across corridors - **Yield Performance**: Monitor returns on treasury holdings - **Cash Flow Forecasting**: Predict funding needs based on historical patterns - **Cost Analysis**: Compare treasury costs across different strategies --- ## Document: Transfers Internal movement of funds URL: /overviews/transfers # Transfers A Transfer is a movement of funds on the Tesser platform among accounts that your organization or your customers own. There are no arms-length third parties involved in a transfer. Transfers involving two treasury/customer wallets are not routed through shield wallets. **Transfers occur during:** - **Rebalancing among treasury wallets:** To ensure sufficient funds in your organization’s wallet(s), you or Tesser with your authorization may redistribute funds among your treasury wallets. - **Funds flow through a shield wallet to/from a Treasury wallet:** When processing a payment, funds will move from a treasury wallet to a shield wallet prior to being distributed to a beneficiary (outbound payment) or from a shield wallet to a treasury wallet (inbound payment). --- ## Document: Supported Tokens and Networks Supported blockchain networks and stablecoins URL: /overviews/tokens-and-networks # Supported Tokens and Networks
**Chain** **Token** **Availability**
POLYGON USDC, USDT Available
ETHEREUM USDC, USDT Coming soon
STELLAR USDC Coming soon
SOLANA USDC, USDT Coming soon
> Note: Need support for other EVM-compatible networks? Contact us and we can quickly add support for additional chains. --- ## Document: Tenants Manage business customers and their integrations URL: /overviews/tenants # Tenants A tenant represents an organization’s customer when that customer is itself a platform with its own users who need to be represented in Tesser’s systems. When an organization’s customers are tenants, originators and/or beneficiaries are customers of the tenant and thus end-customers to the organization. Example structure of resources for an organization with tenants: ![image.png](Tenants/image.png) --- ## Document: Stablecoin Custody Secure non-custodial wallet architecture URL: /overviews/stablecoin-custody # Stablecoin Custody Tesser provides both non-custodial and third-party custodial options for financial institutions to hold and manage stablecoins. We generally recommend a non-custodial model (also called self-custodial) to eliminate dependency on third-party custodians and maintain full operational control. In the self-custodial model, you or your customer is the owner and custodian of the provisioned wallet. This section covers non-custodial (self-custodial) wallets. ## **Wallet Security** Tesser provides enterprise-grade security with a **non-custodial** architecture, meaning: - **Who can custody:** Either your organization or your customers can be the custodians of wallets Tesser provisions. Typically, your organization will custody all the wallets, whether you opt to have a few omnibus wallets for all originator activity or establish one wallet per originator, or a combination of the above. - **You maintain control**: Tesser cannot move your funds without your or your customer’s authorization. For wallets custodied by your customer, your customer can delegate access and authorization for payment instructions to your organization. - **Private key isolation**: Raw private keys are never exposed to Tesser, your software, or your team. - **Secure enclaves**: Private API keys are generated and used for access to the wallet. These keys are generated and stored in hardware-isolated secure enclaves. - **Cryptographic attestation**: Cryptographic proof that only authorized code is running - **Reproducible deployments**: Auditable system configurations with minimal attack surface - **End-to-end audit trail**: Complete cryptographic verification of all operations - **Automated backup management**: We handle secure backup and disaster recovery for your keys - **Multi-factor access controls**: Hardware-backed authentication and authorization - **Quorum-based operations**: Distributed security architecture preventing single points of compromise - **Enclave secure channels**: Direct encrypted communication between secure enclaves and authorized users ## **Wallet Provisioning** During integration, wallets can be created for your team to support testing. Certain payment operations are gated until full production approval is granted. Once in production, wallets can be provisioned on-demand for omnibus treasury management purposes or per customer. Retrieve wallets: ```bash curl -X GET https://api.tesser.xyz/wallets \ -H "Authorization: Bearer your-access-token" ``` Each wallet includes `id`, `workspace_id` ,`address` (and one or more associated token addresses `ata_address`for Solana), and friendly `name` . Optionally, wallets can be assigned to a counterparty by specifying a `counterparty_id` . Wallets can hold balances in more than one stablecoin, denominated by `currency_symbol`, and operate on one or more network, denominated by `network` . --- ## Document: Resources Overview Overview of the Platform API Resources URL: /overviews/resources-overview # Resources Overview The table below outlines the core resources of the Tesser platform and how they relate to each other.
Resource Description
Organization

An organization represents Tesser’s customer, with whom Tesser has a contractual relationship. All other resources belong to an organization.

Workspace

A workspace is a container for counterparties, accounts, payments, and deposits/withdrawals. Users and their roles, API keys, and webhooks are also established at the workspace level.

Every organization starts out with one workspace in the Tesser platform. Additional workspaces can be created by contacting Tesser Customer Support. You may want an additional workspace to segregate activity by line of business, business unit, geography, etc.

Tenant

A tenant represents an organization’s customer when that customer is itself a platform with its own users who need to be represented in Tesser’s systems. When an organization’s customers are tenants, originators and/or beneficiaries are customers of the tenant, and thus end-customers to the organization.

*Note: Most organizations will not need to use tenants to manage their integrations with Tesser.

Counterparty

A counterparty represents an originator (ultimate sender) or beneficiary (ultimate receiver) of a payment. Counterparties can be individuals or businesses. Counterparties are associated with Accounts and belong to a Workspace.

Registering counterparties in advance of submitting payments enables sanction screening and helps meet regulatory requirements. Counterparty information will be transmitted for fiat payouts and, as applicable, for stablecoin payouts to comply with Travel Rule obligations.

Account

An account represents a store of value that holds fiat currency(ies) and/or stablecoin(s). Accounts can belong to a counterparty or to a workspace. Accounts may be external to the Tesser platform (e.g. pre-existing fiat bank accounts or wallets) or provisioned by Tesser (e.g. self-custodial wallets or ledgers at a custodian).

Quote

A quote represents the cost to deliver a payment to a beneficiary. Quotes include the exchange rate between the source currency and the destination currency. Quotes can be based on a "from" (source) amount and currency or a "to" (destination) amount and currency.

Payment

A payment is the movement of funds from an account provisioned by Tesser to a third party, or vice versa. Most commonly, payments will move funds from an originator to a beneficiary. Payments may be outbound (aka payouts) or inbound (receiving stablecoins, aka pay-ins).

Payments also include funds movement to/from a yield provider.

Deposit/Withdrawal

Deposits and withdrawals are first-party movements of funds into or out of liquidity providers.

Deposits can occur by on-ramping funds through a liquidity provider. Withdrawals can occur by off-ramping funds through a liquidity provider.

Rebalance

A rebalance represents funds movement among Tesser-provisioned wallets or ledgers for treasury management purposes.

Step

A step represents funds movement from one account to another (step_type = transfer) or the exchange of one currency for another inside of an account (step_type = swap).

--- ## Document: Payments Movement of funds to/from counterparties URL: /overviews/payments # Payments A payment is the movement of funds from an account provisioned by Tesser to a third party, or vice versa. Most commonly, payments will move funds from an originator to a beneficiary. Payments may be outbound (aka payouts) or inbound (receiving stablecoins, aka pay-ins). Payments also include funds movement to/from a stablecoin yield provider. **Shield wallets in payments processing** Payments always involve a counterparty that is not you or your customer (direct or end-customer):
**Originator is you or your customer/end-customer** **Beneficiary is you or your customer/end-customer**
**Outbound payment**
**Inbound payment**
Because of these arms-length counterparties, Tesser processes payments through “shield” wallets. When funds flow through shield wallets, your organization’s treasury wallets are protected from direct exposure to wallets with potentially risky activity. This means your treasury wallets never directly transact with a wallet that is, or later becomes, tainted with undesirable activity. Outbound payments: ![image.png](./payments1.png) Inbound payments: ![image.png](./payments2.png) --- ## Document: Payment Planning Quotes, costs, and routing for payments URL: /overviews/payment-planning # Payment Planning As you submit information about the payment request, Tesser will update the plan for the payment. **Cost information** Once you supply information about the `from_currency`, `to_currency`, amount (one of `from_amount` or `to_amount`), and, if a stablecoin payout, the `from_network` and `to_network`, Tesser will respond with a quote for costs associated with the payment. A cost quote includes the following elements: - **Exchange rate**: The quote locks in an exchange rate between the `from_currency` and `to_currency`. For stablecoin payouts, when the source currency of the originator and destination currency of the beneficiary are both stablecoins, the exchange rate will always be 1:1. - *Note: Exchange rates are implicit based on the `from_amount` and `to_amount` provided in the response to the payment creation request.* - **Requested source or destination amount:** You may request a quote based on the source (from) amount to be sent or the destination (to) amount to be received. For instance, if an originator wants to guarantee a certain amount of MXN to deliver to a beneficiary, you can request a quote for the necessary source amount of a stablecoin given the destination amount and currency. Alternatively, if an originator has a defined amount of USDC to send, the quote will provide information about how much MXN will be delivered to the beneficiary. - **Other fees:** Tesser will supply details on the network or provider fees that can be known prior to payment execution. *Note that blockchain network (gas) fees are only determined at the time of payment execution. After payment execution, gas fee info will be updated on the payment record.* - Fee information is stored as an array in the `fees` [object](/api/~schemas#payment-fee) for each step for the payment. Each fee will include information on the `fee_amount`, `fee_currency`, `fee_type`, and optionally `fee_metadata`. **Payment expiration** Payments are valid for a certain amount of time. The length may vary by currency(ies), from/to country, and amount to be paid out. When the expiration time elapses, a new payment must be created in order for the payment to be processed. The time when a payment expires is indicated by `expires_at`. > All payments will have an expiration length, even stablecoin payments. This is because risk and compliance checks must be re-initiated after a certain amount of time has passed if the payment has not been executed. > **Routing** Once you supply information about the `funding_account_id`, `from_account_id`, and `to_account_id`, Tesser will respond with the route plan to execute the payment. - **Transfer steps:** The plan will indicate the route the payment will take to deliver funds to the beneficiary. - Stablecoin payouts will always include at least one step of type `transfer`, and potentially more if token or network swaps are required. - Fiat payouts will always include at least two steps (on-chain `transfer` to an off-ramp and last-mile `transfer` using a local payment network). --- ## Document: Networks Supported blockchain networks URL: /overviews/networks # Networks import { Badge } from "zudoku/ui/Badge"; import { Callout } from "zudoku/ui/Callout"; ## Networks The Networks resource provides information about the blockchain networks supported by the platform. ### Overview We support a curated list of major blockchain networks for crypto transactions. This resource allows you to query the available networks dynamically. We recommend dynamically querying the `/networks` endpoint rather than hardcoding values, as supported networks may expand over time. ### Supported Networks Commonly supported networks include: - ETHEREUM **Ethereum** - POLYGON **Polygon** - STELLAR **Stellar** - SOLANA **Solana** See the [API Reference](/api#tag/Networks) for the live list of supported networks. --- ## Document: Liquidity Providers Integration with global fiat on/off ramps URL: /overviews/liquidity-providers # Liquidity Providers Tesser partners with multiple liquidity providers, also known as on/off ramps, to provide conversion capabilities between stablecoins and fiat currencies. ## **Geographic Coverage** Tesser can tap into liquidity in any region through a global provider network. We activate new corridors with short turnaround based on customer demand. > Need coverage in another region? Contact us and we can quickly add support for additional markets. ## **Onboarding Experience** - If your organization has any existing provider integrations, for example, Circle for USDC, we will reuse those. - For a new provider, where possible, Tesser provides an embedded onboarding experience. For embedded onboarding, your organization will not have to contract with the liquidity provider or proceed through their onboarding flows. Instead, Tesser will use information already collected about your organization and request any additional information needed through the Tesser dashboard or API. ## **On-Ramping** When making a fiat payout to a beneficiary, on-ramps will typically be used to convert fiat to stablecoins: - **Depositing instructions**: Tesser will generate and communicate instructions specific to each deposit for you to follow. The supported funds delivery mechanisms to an on-ramp are dependent on the local market; on-ramps typically accept push methods (e.g. ACH credit or Wire in the U.S.) - **Same-Day Settlement**: Fast conversion for operational efficiency. *Settlement timing is dependent on the local market and may only be available during business hours* - **Competitive Rates**: Institutional-grade FX rates - **Automated Processing**: Scheduled conversions based on payment needs - **Funds distribution:** On-ramped funds can be deposited into one or more wallets that you specify during the deposit instruction process ## **Off-Ramping** As part of Treasury management, your organization may off-ramp funds back into your local fiat currency based on adjusted forecasts. When making a fiat payout to a beneficiary, off-ramps in the destination country will be used to convert stablecoins to the local fiat currency: ### **Payout Methods to Beneficiaries** Currently available: - **Bank Transfers**: Direct deposit to recipient bank accounts - **Digital Wallets**: Local e-wallet integrations Coming soon: - **Mobile Money**: Integration with mobile payment systems (under development) - **Cash Pickup**: Physical cash collection points (under development) ### **Key Benefits** - **Competitive Rates**: Market-leading conversion rates - **Real-time funds delivery:** When real-time payment methods are available in the local market, funds can be delivered to recipients instantly - **Detailed Reporting**: Complete reconciliation data for all conversions --- ## Document: Funds Movement Lifecycle and Data Model How Tesser models payments, deposits, withdrawals, and rebalances — the shared data shape, lifecycle phases, and statuses. URL: /overviews/funds-movement-lifecycle-and-data-model # Funds Movement Lifecycle and Data Model Tesser models funds movement using a single shared data shape and lifecycle. The resource types differ mainly in their **direction** and which **pre-execution checks** apply. Their data model, step structure, status taxonomy, and webhook conventions are the same. ## Resource Types Tesser exposes four funds-movement resources (payments, deposits, withdrawals, and rebalances). Each is modeled with the same shared envelope (described in [Resource Data Model](#resource-data-model)) and progresses through the same [Lifecycle Phases](#lifecycle-phases). - **Payment** — funds movement to or from an external counterparty. Payouts (`direction` = outbound) are created via [POST /v1/payments](/api/payments#create-payment). Inbound payments (`direction` = inbound) cannot currently be pre-registered but will be recorded and surfaced via webhooks and the API. - **Deposit** — inbound funds movement that on-ramps fiat from your bank to a Tesser-managed account, with optional conversion into stablecoin at a liquidity provider. Created via [POST /v1/treasury/deposits](/api/treasury#create-deposit). - **Withdrawal** — outbound funds movement that off-ramps stablecoin held at Tesser-managed accounts back to your bank. Created via [POST /v1/treasury/withdrawals](/api/treasury#create-withdrawal). - **Rebalance** — internal funds movement between two managed accounts (provider ledger to wallet, wallet to wallet, ledger to ledger). Created via [POST /v1/treasury/rebalances](/api/treasury#create-rebalance). ## Resource Data Model Each resource type is represented as a single JSON object with a consistent set of top-level fields, a `desired` / `estimated` / `actual` overlay, and the `steps[]` array. Resource-specific fields (`risk_status`, `balance_status`, `funding_account_id`) are added as applicable at the top-level of the record — see the [Resource-Specific fields](#resource-specific-fields) for which applies where. ### Shared Top-Level Fields All four resource types include the following top-level fields: - `id` — Tesser-assigned UUID for the resource. - `workspace_id` — UUID of the workspace that owns the resource. - `organization_reference_id` — optional client-supplied identifier (e.g., your internal payment or treasury operation ID). - `direction` — one of `outbound`, `inbound`, or `rebalance`. See the matrix below for which value each resource takes. - `created_at` — timestamp the resource was created at Tesser. - `updated_at` — timestamp of the most recent change to the resource. - `expires_at` — timestamp after which the resource will no longer execute. See [Expiration](#expiration). - `steps[]` — ordered array of execution steps Tesser plans for the resource. Populated shortly after creation and supplied via the `.quote_created` webhook. - `desired` / `estimated` / `actual` — the [overlay](#the-desired--estimated--actual-overlay) describing intent, projection, and outcome. Each contains `from` and `to` fields. ### Resource-Specific Fields Some fields and concepts apply only to a subset of resource types. The table below summarizes which top-level concepts appear on which resources. | Field / Concept | Payment | Deposit | Withdrawal | Rebalance | |---|---|---|---|---| | `risk_status` (risk check) | yes | no | no | no | | `balance_status` (balance check) | yes | no | yes | yes | | `funding_account_id` | yes | no | no | no | | `direction` value | `outbound`, `inbound` | `inbound` | `outbound` | `rebalance` | - **Risk check** screens the external sending or receiving wallet of a payment. - **Balance check** reserves funds in the `desired.from.account_id`; applied whenever funds are debited from a managed account (outbound payments, withdrawals, rebalances). - **`funding_account_id`** is unique to outbound payments — it identifies which source bank account ultimately funds the payout. See [Travel Rule](/overviews/compliance-and-risk#travel-rule). The skeletons below show how those resource-specific differences manifest in the top-level shape of each non-payment resource. Overlay sub-objects and `steps[]` are elided — see [The Desired / Estimated / Actual Overlay](#the-desired--estimated--actual-overlay) below for the overlay shape and the [full payment example](#example-outbound-payment) further down for everything in context. `direction: "inbound"`. No risk fields, no `balance_status`, no `funding_account_id`. ```jsonc { "id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { /* from / to */ }, "estimated": { /* from / to */ }, "actual": { /* from / to */ }, "steps": [ /* one or more step objects */ ], "created_at": "2025-12-02T14:00:00.000Z", "updated_at": "2025-12-02T14:30:00.700Z", "expires_at": "2025-12-03T14:00:00.000Z" } ``` `direction: "outbound"`. Adds `balance_status` and `balance_reserved_at`. No risk fields, no `funding_account_id`. ```jsonc { "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { /* from / to */ }, "estimated": { /* from / to */ }, "actual": { /* from / to */ }, "steps": [ /* one or more step objects */ ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.300Z", "expires_at": "2025-12-02T10:00:00.000Z" } ``` `direction: "rebalance"`. Adds `balance_status` and `balance_reserved_at`. No risk fields, no `funding_account_id`. ```jsonc { "id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { /* from / to */ }, "estimated": { /* from / to */ }, "actual": { /* from / to */ }, "steps": [ /* one or more step objects */ ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:30:00.200Z", "expires_at": "2025-12-02T10:00:00.000Z" } ``` By comparison, the [outbound payment example](#example-outbound-payment) below adds `risk_status`, `risk_status_reasons`, `risk_reviewed_by`, `risk_reviewed_at`, `funding_account_id`, plus `balance_status` and `balance_reserved_at` at the top level. ### The Desired / Estimated / Actual Overlay Funds movement at scale involves uncertainty: liquidity providers may not guarantee exchange rates, on-chain transactions may fail, and resources can terminate at intermediate steps if they don't complete by `expires_at`. To handle this gracefully, every resource carries three parallel snapshots of its `from` and `to` fields. - **Desired** — your stated intent at creation: the source and destination accounts, currencies, networks, and amount. Set on the create request and **never overwritten** afterward. This preserves intent for downstream reconciliation regardless of how the resource ultimately concludes. - **Estimated** — Tesser's best projection for how the resource will proceed if it succeeds. Populated after planning and quoting (see [Planning](#planning)). For resources that involve a swap or cross-currency step, the ratio of `estimated.from.amount` to `estimated.to.amount` is the indicative exchange rate. For same-currency moves, `estimated.*` matches `desired.*` (1:1). - **Actual** — what actually settled. Populated as steps complete and at the resource level when the resource reaches a terminal state. On success, `actual.*` matches `estimated.*`. On failure, `actual.*` reflects the final state — possibly different account, currency, or network than `desired.*`. Each overlay contains a `from` and `to` field with the same four sub-fields: - `account_id` — the Tesser account UUID for that step. - `amount` — string-encoded decimal amount. - `currency` — currency code (e.g., `USD`, `USDC`, `MXN`). - `network` — blockchain network code (e.g., `BASE`, `POLYGON`), or `null` for non-network steps (bank accounts, provider ledgers). #### Overlay applicability The desired fields are only included on the top-level of the record. The estimated and actual overlay structure is repeated on each step. | Overlay | Top-Level | Step | |---|---|---| | Desired | Yes | No | | Estimated | Yes | Yes| | Actual | Yes | Yes| The `estimated.from.*` fields at the top level of the record are populated from the `desired.from.*` fields. The `estimated.to.*` fields represent Tesser's best estimate for the outcome of last step of the resource. ### Steps The `steps[]` array represents the ordered sequence of steps Tesser plans to execute. Each step has the `estimated` and `actual` overlay, plus a `status` and timestamps for each phase transition. Two step types are currently used across all resource types: - `transfer` — funds move from one account to another. The source and destination accounts may differ in currency or network. Examples: an on-chain stablecoin transfer between two wallets; a fiat push from a bank to a liquidity provider's bank; a last-mile fiat payout via a local payment network. - `swap` — currencies are exchanged within the same account (the step's `estimated.from.account_id` and `estimated.to.account_id` are equal). Example: at a liquidity provider, selling USD and buying USDC inside the same provider ledger. Each step has a status. See [Step Statuses](#step-statuses) for more information. ### Example: Outbound Payment The JSON below shows the full shape of a successful USDC (Ethereum) → MXN payout, captured at terminal state. Note how the `desired` overlay appears only at the top level, while `estimated` and `actual` repeat on each step in `steps[]`. Resource-specific fields (`risk_status`, `balance_status`, `funding_account_id`) are all populated for outbound payments — compare against the deposit, withdrawal, and rebalance skeletons in [Resource-Specific Fields](#resource-specific-fields) above. Because this payout routes through a stablecoin off-ramp provider that quotes a deterministic delivery amount, `actual.to.amount` matches `estimated.to.amount` exactly; the only overlay divergence visible is on the source side, where `desired.from.amount` is `null` (the request fixed the `to.amount`) and `estimated.from.amount` carries Tesser's quote.
View full JSON resource ```json { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "transaction_hash": "0x1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "turnkey", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:02.000Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": "2025-12-01T09:00:02.000Z", "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "transaction_hash": null, "fees": [ { "fee_amount": "1.50", "fee_currency": "USDC", "fee_type": "provider", "fee_metadata": {} } ], "provider_key": "alfred", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:02.500Z", "signature_requested_at": null, "signed_at": null, "submitted_at": "2025-12-01T09:00:02.100Z", "confirmed_at": "2025-12-01T09:00:02.300Z", "completed_at": "2025-12-01T09:00:02.500Z", "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:02.500Z", "expires_at": "2025-12-01T23:59:59.999Z" } ```
For the full webhook sequence leading to this terminal state, see [Create a payout from a wallet](/how-tos/send-a-stablecoin-payout/create-a-payout-from-a-wallet). ### Fees Fees are reported as an array in the per-step `fees[]` object. Fees include blockchain network (gas) fees or provider fees. Each entry includes: - `fee_amount` - `fee_currency` - `fee_type` - (Optional) `fee_metadata` :::note Depending on the type, the amount of a fee may not be known until the step has executed (e.g. gas fees). Other times, the fee may be known in advance (e.g. if a fiat off-ramp charges a separate transaction fee for a payout). ::: ## Lifecycle Phases Every resource progresses through four phases: planning, pre-execution checks, execution, and terminal state. Each phase emits webhooks; the prefix is the resource type (`payment.*`, `deposit.*`, `withdrawal.*`, `rebalance.*`). ### Planning When you POST the create request, Tesser creates the resource synchronously and begins to plan the sequence of steps to execute the request, including the quote. The quote includes: - **Exchange rate** — The ratio of `estimated.from.amount` to `estimated.to.amount` is the indicative exchange rate between `estimated.from.currency` and `estimated.to.currency`. - For stablecoin-to-stablecoin same-currency transfers, the rate is always 1:1. - For cross-currency transfers, Tesser sources the best exchange rate from available liquidity providers. - **From or To amount** — For payments, you may request a quote based on the amount to send (specify `desired.from.amount`) or the to amount to receive (specify `desired.to.amount`). Tesser determines the other side of the overlay based on the prevailing rate. For deposits, withdrawals, and rebalances, you specify the `desired.from.amount`. Tesser then fires the `.quote_created` webhook. This event **always fires**, even for same-token, same-network moves, to keep the lifecycle uniform across resource types. The `.quote_created` webhook contains the planned `steps[]` array. Each step has a `status` of `created`. At this point the `desired` overlay is populated on the top-level of the resource. The `estimated` overlay is populated at the top level and on each step. `actual.*` fields are all null. ### Pre-Execution Checks Before steps begin executing, Tesser runs the checks that apply to the resource (per the [Resource-Specific fields](#resource-specific-fields) above). - **Risk check** (payments only) — screens the destination wallet for sanctions and other risk signals. The `payment.risk_updated` webhook reports the outcome via `risk_status` (see [Risk Statuses](#risk-statuses)). If your organization's policy requires manual review for the result, a user with sufficient permissions can review the payment in the Tesser dashboard or submit a decision via [`POST /v1/payments/{paymentId}/risk-review`](/api/payments#submit-risk-review). :::note For inbound payments, you do not have to report your decision to the risk review API, but it is recommended so that you can keep your transaction history in Tesser's platform reconcilable and auditable. ::: - **Balance check** (payments, withdrawals, rebalances) — reserves source funds on the resource's source account. Two outcomes are possible: `reserved` (sufficient funds; the resource proceeds to execution) or `awaiting_funds` (insufficient funds; the resource is queued and will retry until `expires_at`). Reported via `.balance_updated`. Where the source account is a **provider ledger** (e.g. Circle ledger, OpenFX ledger), Tesser performs the balance check automatically by reading the ledger's available balance. Where the source account is a **self-custodial wallet**, the balance check is part of the step-signing process: Tesser emits `step.signature_requested`, you sign the step locally and submit the signature, and the synchronous response plus the subsequent `.balance_updated` webhook reflect the reservation outcome. ### Execution Once pre-execution checks pass, Tesser executes each step in order. Steps within the same resource may overlap (a fiat off-ramp step may begin before a preceding on-chain step has fully finalized), but each individual step transitions through the same status states in sequence. The step status progression when on-chain signing is not applicable is: ``` created → submitted → confirmed → completed ↘ failed ``` The step status path when on-chain signing is required is: ``` created → signature_requested → signed → submitted → confirmed → completed ↘ failed ``` Each transition emits a `step.` webhook. See [Step Statuses](#step-statuses) for definitions. :::note Sometimes, a step's `status` will progress directly from `created` to `completed` or `failed`. This can happen when providers emit limited webhooks to Tesser, which reduces the visibility Tesser has to granular intermediate movements. ::: ### Terminal State and Divergence When the last step reaches a terminal state, Tesser populates the top-level `actual.*` overlay and emits a `.updated` webhook carrying the full updated resource object. You can use this webhook to observe the terminal state without GET-ing the resource. - **`completed`** — `actual.from.*` and `actual.to.*` are populated. Where the resource includes a swap step, the actual exchange rate may differ slightly from the indicative quote. - **`failed`** — `actual.*` reflects what actually occurred. The resource may have terminated mid-route, leaving funds at an intermediate account in an intermediate currency. For example, an on-ramp deposit that fails at its swap step terminates with `actual.to.currency = "USD"` (fiat at the provider's ledger) even though `desired.to.currency = "USDT"`. ## Statuses Reference This section catalogs the status taxonomies used across the lifecycle. ### Risk Statuses Result of risk review performed on the destination wallet. Risk statuses appear on payments only; see the [Resource-Specific fields](#resource-specific-fields). | Status | Terminal | Webhook event type | Description | |---|---|---|---| | `unchecked` | No | Not sent. All payments at creation have a risk status of `unchecked`. | Beneficiary wallet identifier has not been supplied or risk check has not yet completed. | | `awaiting_decision` | No | `payment.risk_updated` | Beneficiary wallet has been risk screened and requires manual review to determine whether to send the payout. | | `auto_approved` | Yes | `payment.risk_updated` | Beneficiary wallet has been risk screened and automatically approved per your organization's policy. | | `manually_approved` | Yes | `payment.risk_updated` | Beneficiary wallet has been risk screened and has been manually reviewed and approved. | | `auto_rejected` | Yes | `payment.risk_updated` | Beneficiary wallet has been risk screened and automatically rejected per your organization's policy. | | `manually_rejected` | Yes | `payment.risk_updated` | Beneficiary wallet has been risk screened and has been manually reviewed and rejected. | ### Balance Statuses Result of the balance check on the source account. Balance statuses appear on payments, withdrawals, and rebalances; see the [Resource-Specific fields](#resource-specific-fields). The webhook event type uses the resource-type prefix — e.g. `payment.balance_updated`, `withdrawal.balance_updated`, `rebalance.balance_updated`. | Status | Terminal | Webhook event type | Description | |---|---|---|---| | `unreserved` | No | Not sent. All resources at creation have a balance status of `unreserved`. | Source account has not been supplied or the reserve operation has not yet completed. | | `awaiting_funds` | No | `.balance_updated` | The balance of the source account was checked and there are insufficient funds. The resource is queued and awaits funding (e.g., from a deposit) until `expires_at`. | | `reserved` | Yes | `.balance_updated` | The balance of the source account was checked and funds were reserved to process the resource. | ### Step Statuses Result of step execution. Step statuses and their webhooks (`step.*`) are shared across all four resource types and all step types (`transfer`, `swap`, future types). | Status | Terminal | Webhook event type | Description | |---|---|---|---| | `created` | No | Not sent. All steps at creation have a status of `created`. | Tesser has created a record for this step. | | `signature_requested` | No | `step.signature_requested` | Signature has been requested for the step | | `signed` | No | `step.signed` | Step has been cryptographically signed | | `submitted` | No | `step.submitted` | Tesser submitted the step information to the blockchain or fiat payment network. | | `confirmed` | No | `step.confirmed` | The step was accepted by the operator of the payment network. | | `completed` | Yes | `step.completed` | The step delivered funds to the destination account. For fiat steps, indicates the local payment network has delivered funds. Some fiat networks may not provide formal confirmation, in which case funds are assumed delivered unless `failed` is indicated. | | `failed` | Yes | `step.failed` | The step was not successful; funds were not transferred from the source to the destination. | :::note Some step types collapse to a terminal-only `step.completed` (or `step.failed`) — for these, Tesser doesn't fire intermediate `step.submitted` or `step.confirmed` events. See [Execution](#execution). ::: ## Expiration Every resource has an `expires_at` timestamp. The validity period varies by resource type, currency pair, route, and amount. After expiration, the resource will not execute and a new resource must be created to retry. :::warning{title="Why resources expire"} Even same-currency, same-network resources have an `expires_at`. Risk and compliance checks must be re-run after a certain amount of time has passed if the resource has not been executed, and cross-currency transfers need their quotes refreshed. ::: A resource enters the expired state when its `expires_at` passes before it has reached a completed terminal state. This can happen when steps were planned but didn't all reach `completed` in time, or when steps were never planned at all — for example, when no provider quote could be sourced before expiration. Concrete examples: a payment whose `balance_status` never reached `reserved` because the `desired.from.account_id` was not replenished in time; a deposit whose terminal step never confirmed; a withdrawal that expired before any provider quote was available. When this happens, Tesser emits a `.expired` webhook (`payment.expired`, `deposit.expired`, `withdrawal.expired`, `rebalance.expired`) carrying the resource's final state. The `actual.*` overlay on the payload reflects the last observed state. You can use this webhook to observe the expiration without GET-ing the resource — see [Payment Updates](/webhooks/payment-updates) and [Treasury Updates](/webhooks/treasury-updates) for the full event tables. To retry, create a new resource with fresh `desired.*` fields; you cannot resurrect an expired resource. --- ## Document: Errors Complete reference of all API error codes and their meanings URL: /overviews/errors # Errors {/* AUTO-GENERATED - DO NOT EDIT. Run: bun run apps/gateway/docs/scripts/generate-error-docs.ts */} ## Error Response Format All API errors return a consistent JSON response: ```json { "errors": [ { "error_code": "domain-YZZZ", "error_message": "Human-readable error description" } ] } ``` ## Error Code Convention Error codes follow the format `{domain}-{YZZZ}` where: - **domain** identifies the resource area (e.g., `accounts`, `payments`, `treasury`) - **Y** indicates the HTTP status category - **ZZZ** is the specific error number within that category | Range | HTTP Status | Meaning | | --- | --- | --- | | 1000–1999 | 404 | Not Found | | 2000–2999 | 401 / 403 | Unauthorized / Forbidden | | 3000–3999 | 400 | Bad Request | | 4000–4999 | 429 | Too Many Requests | | 5000–5999 | 502 / 503 | Bad Gateway / Service Unavailable | :::note Some domains (Circle, Idempotency) use legacy numbering that does not follow the range convention above. ::: ## Accounts
Error Code HTTP Status Error Message Description
`accounts-1000` 404 Account with id '{id}' not found The specified account ID does not exist in this workspace
`accounts-1001` 404 Entity with id '{id}' not found The tenant or counterparty specified does not exist
`accounts-2000` 403 The entity does not belong to your workspace User tries to link an account to an entity that belongs to a different workspace
`accounts-3000` 400 Cannot provide both tenant_id and counterparty_id User provides both tenant_id and counterparty_id when only one is allowed
`accounts-3001` 400 A workspace-level bank account already exists. Only one is allowed per workspace. User tries to create a second workspace-level bank account
`accounts-3002` 400 wallet_address is required for unmanaged wallets (is_managed=false) User creates an unmanaged wallet without providing a wallet_address
`accounts-3003` 400 Cannot determine network for wallet type '{type}' The provided wallet type does not map to a known blockchain network
`accounts-3004` 400 signature is required for managed wallets (is_managed=true) User creates a managed wallet without providing the required signature
`accounts-3005` 400 Invalid signature format. Expected base64-encoded JSON with body and stamp fields. The signature parameter could not be decoded or is missing required fields
`accounts-3006` 400 Circle Mint ledgers require a tenant_id or counterparty_id. Workspace-level sub-ledgers are not supported. User tries to create a Circle Mint ledger without linking it to a tenant or counterparty
`accounts-3007` 400 This entity already has a Circle Mint ledger. Only one Circle Mint ledger per tenant/counterparty is allowed. User tries to create a duplicate Circle Mint ledger for the same entity
`accounts-3008` 400 A workspace-level Circle Mint master wallet ledger already exists. User tries to create a second master wallet ledger for the workspace
`accounts-3009` 400 CIRCLE_MINT provider requires a tenant_id or counterparty_id Circle Mint metadata preparation requires an entity to be linked
`accounts-3010` 400 CIRCLE_MINT provider requires a business entity (not individual) Circle Mint only supports business-classified entities, not individuals
`accounts-3011` 400 Entity is missing required fields for Circle The entity does not have all required business fields populated for Circle onboarding
`accounts-3012` 400 Cannot create VAN: compliance state is not ACCEPTED Virtual account number creation requires the account to have ACCEPTED compliance state
`accounts-3013` 400 Cannot create VAN: circle_wallet_id is missing from account metadata The ledger account does not have a Circle wallet ID configured
`accounts-3014` 400 No workspace-level bank account found. Please create a bank account first. A workspace-level bank account is required but none exists
`accounts-3015` 400 No wire bank accounts found in Circle. Please register a bank account on Circle dashboard first. Circle does not have any wire bank accounts registered for this workspace
`accounts-3016` 400 Multiple wire bank accounts found in Circle Production requires exactly one bank account in Circle, but multiple were found
`accounts-3017` 400 Could not determine Circle bank ID Failed to resolve the Circle bank ID needed for wire transfers
`accounts-3018` 400 Unsupported provider. Must be one of : CIRCLE_MINT, KRAKEN The specified provider is not supported for this operation. Must be one of : CIRCLE_MINT, KRAKEN
`accounts-3019` 400 Account does not have a provider configured The account metadata does not contain provider configuration
`accounts-3020` 400 Entity type must be 'counterparty' or 'tenant' The referenced entity is not a valid type for account linking
`accounts-3022` 400 Signature body has unexpected activity type '{actual}', expected '{expected}' The stamped Turnkey activity inside the signature is not the expected CREATE_WALLET activity
`accounts-3023` 400 Signature body organizationId does not match the authenticated workspace's Turnkey sub-organization The caller stamped an activity targeting a sub-organization that does not belong to their workspace
`accounts-3024` 400 Signed wallet account addressFormat '{actual}' does not match type '{type}' (expected '{expected}') The stamped CREATE_WALLET parameters describe an address format that does not match the dto.type
`accounts-3025` 400 Managed wallets are only supported for type 'stablecoin_ethereum' (got '{type}') User attempted to create a managed (is_managed=true) wallet for a non-Ethereum wallet type
`accounts-3100` 400 account_name is required and must be between 1-255 characters User provides an account name that doesn't meet length requirements
`accounts-3101` 400 tenant_id must be a valid UUID User provides a tenant_id that is not a valid UUID
`accounts-3102` 400 counterparty_id must be a valid UUID User provides a counterparty_id that is not a valid UUID
`accounts-3103` 400 bank_name is required and must be between 1-255 characters User provides a bank name that doesn't meet length requirements
`accounts-3104` 400 bank_code_type must be one of: SWIFT, BIC, IBAN, ROUTING User provides a bank_code_type that is not in the allowed enum
`accounts-3105` 400 bank_identifier_code is required User creates a bank account without providing the bank identifier code
`accounts-3106` 400 bank_account_number is required User creates a bank account without providing the account number
`accounts-3107` 400 type must be one of: stablecoin_ethereum, stablecoin_solana, stablecoin_stellar User provides a wallet type that is not in the allowed enum
`accounts-3108` 400 wallet_address must be a valid blockchain address for the specified network User provides a wallet address that doesn't match the expected format for the network
`accounts-3109` 400 Wallet signature is malformed or invalid User provides a signature that is not properly formatted or cannot be verified
`accounts-3110` 400 provider must be one of: CIRCLE_MINT, KRAKEN User provides a provider value that is not in the allowed enum
`accounts-5000` 502 Failed to create wallet. Please try again or contact support if the issue persists. Turnkey or other wallet provider failed during wallet creation
`accounts-5001` 502 Failed to create Circle external entity Circle API call to create the external entity failed
`accounts-5002` 502 Unable to create virtual account number (VAN) for this ledger account. Circle API call to create the virtual account number failed
`accounts-5003` 502 Failed to retrieve entity business fields from vault Basis Theory vault call to retrieve entity fields failed
## Counterparties
Error Code HTTP Status Error Message Description
`counterparties-1000` 404 Counterparty not found The specified counterparty ID does not exist
`counterparties-1001` 404 Tenant with id '{id}' not found. Cannot assign counterparty to non-existent tenant. User tries to assign a counterparty to a tenant_id that doesn't exist
`counterparties-2000` 403 You do not have access to this counterparty User tries to access a counterparty that belongs to a different workspace
`counterparties-3002` 400 classification must be 'individual' or 'business' User provides a classification value that is not 'individual' or 'business'
`counterparties-3003` 400 business_legal_name is required for business counterparties and must be 1-255 characters User creates a business counterparty with missing or improperly formatted business_legal_name
`counterparties-3004` 400 business_address_country is required for business counterparties and must be a valid ISO 3166-1 alpha-2 country code User provides a business_address_country that is not a valid 2-letter country code (e.g., "US", "GB")
`counterparties-3005` 400 individual_first_name is required for individual counterparties and must be 1-255 characters User creates an individual counterparty with missing or improperly formatted first name
`counterparties-3006` 400 individual_last_name is required for individual counterparties and must be 1-255 characters User creates an individual counterparty with missing or improperly formatted last name
`counterparties-3007` 400 individual_address_country is required for individual counterparties and must be a valid ISO 3166-1 alpha-2 country code User provides an individual_address_country that is not a valid 2-letter country code
`counterparties-3008` 400 individual_date_of_birth must be a valid date in YYYY-MM-DD format User provides a date_of_birth that doesn't match the required format or is not a valid date
`counterparties-3009` 400 tenant_id must be a valid UUID User provides a tenant_id that is not a validly formatted UUID
`counterparties-3010` 400 individual_street_address1 is required for individual counterparties User creates an individual counterparty without providing the street address
`counterparties-3011` 400 individual_city is required for individual counterparties User creates an individual counterparty without providing the city
`counterparties-3012` 400 individual_postal_code is required for individual counterparties User creates an individual counterparty without providing the postal code
`counterparties-3013` 400 business_street_address1 is required for business counterparties User creates a business counterparty without providing the street address
`counterparties-3014` 400 business_city is required for business counterparties User creates a business counterparty without providing the city
`counterparties-3015` 400 business_postal_code is required for business counterparties User creates a business counterparty without providing the postal code
## Currencies
Error Code HTTP Status Error Message Description
`currencies-1000` 404 Currency not found The specified currency code does not exist
`currencies-3000` 400 Invalid currency code format User provides a currency code that is not a valid ISO 4217 format (e.g., USD, EUR, GBP)
`currencies-3001` 400 Currency is not supported for this operation User attempts to use a currency that is valid but not supported for the requested operation
`currencies-3002` 400 Invalid currency pair for exchange User attempts to exchange between currencies where the pair is not supported
## Networks
Error Code HTTP Status Error Message Description
`networks-1000` 404 Network not found The specified network identifier does not exist
`networks-3000` 400 Invalid network identifier User provides a network identifier that is not a valid format (e.g., ethereum, polygon, base)
`networks-3001` 400 Network is not supported for this operation User attempts to use a network that is valid but not supported for the requested operation
`networks-3002` 400 Currency is not compatible with the specified network User provides a currency/network combination that is not compatible (e.g., BTC on Ethereum network)
`networks-3003` 400 Network is currently unavailable or under maintenance User attempts to use a network that is temporarily unavailable
`networks-5000` 502 Failed to connect to network RPC endpoint Connection to the blockchain network RPC endpoint failed due to external service error
## Payments
Error Code HTTP Status Error Message Description
`payments-1000` 404 Payment not found The specified payment ID does not exist
`payments-3000` 400 from_network must equal to_network User provides different networks for from_network and to_network
`payments-3001` 400 invalid from_amount or to_amount from_amount and to_amount should be positive and be valid numbers
`payments-3002` 400 Either from_amount or to_amount must be provided. Cannot create payment without an amount. User does not provide either from_amount or to_amount
`payments-3003` 400 Only one of from_amount or to_amount should be provided. The other will be calculated using the exchange rate. User provides both from_amount and to_amount when only one should be provided
`payments-3004` 400 from_network is required for crypto-to-crypto payments User creates a payment with stablecoin from_currency but does not provide from_network
`payments-3005` 400 to_network is required for crypto-to-crypto payments User creates a payment with stablecoin to_currency but does not provide to_network
`payments-3006` 400 Onramp (fiat-to-crypto) payments are not yet supported. Coming soon! User attempts to create an onramp payment which is not supported yet
`payments-3007` 400 Invalid from_currency User provides a from_currency that is not supported
`payments-3008` 400 Invalid to_currency User provides a to_currency that is not supported
`payments-3009` 400 funding_account_id not found User provides a funding_account_id that does not exist
`payments-3010` 400 to_account_id not found User provides a to_account_id that does not exist
`payments-3011` 400 from_account_id not found User provides a from_account_id that does not exist
`payments-3012` 400 Missing input parameter is_approved User submits a payment review without the required is_approved parameter
`payments-3013` 400 signature is malformed or signed with incorrect key User provides a signature that is malformed or not signed with the correct key
`payments-3014` 400 The signed transaction does not match the details of the payment User provides a signature that does not match the payment details
`payments-3015` 400 The exchange rate quote has expired. Please create a new payment to get a fresh quote. User attempts to execute a payment with an expired exchange rate quote (valid for 24 hours)
`payments-3016` 400 Payment quote expiration data is missing. Please create a new payment to get a fresh quote. Payment has an exchange rate but is missing expiration data, indicating a data integrity issue
`payments-3017` 400 to_account has not yet been risk approved by custodian Beneficiary account has a Circle recipient that is still pending verification or has not been registered yet
`payments-3018` 400 from_account_id and to_account_id should not both be managed accounts for payments. Use /v1/treasury/rebalances instead. Transfers between two managed accounts are treasury operations. Use /v1/treasury/rebalances instead.
`payments-3019` 400 Account asset not found for {from_account_id} No matching account asset found for the from account with the specified currency and network
`payments-3020` 400 No {currency} balance found on network {network} for {to_account_id} The to account does not have a balance for the specified currency and network
`payments-3021` 400 Amounts not yet calculated. Payment may still be processing. Payment amounts have not been calculated yet, the payment may still be processing
`payments-3022` 400 Payment missing currency information Payment is missing required currency or network data
`payments-3023` 400 Payment has not passed compliance screening. Cannot execute. Payment has not been approved through compliance/risk screening
`payments-3024` 400 Payment does not have reserved balance. Cannot execute. Payment balance has not been reserved prior to execution
`payments-3025` 400 No transfer step found for payment Payment does not have a transfer step
`payments-3026` 400 Transfer step is already in status {status}. Cannot execute. Transfer step is in a status that does not allow execution
`payments-3027` 400 Transfer step missing account asset IDs. Cannot execute. Transfer step is missing the required from or to account asset IDs
`payments-3028` 400 Could not load accounts for execution Failed to load the from or to account required for payment execution
`payments-3029` 400 Payment missing required data for step creation Payment is missing required currency, amount, or network information for step creation
`payments-3030` 400 To asset is required for onchain payments Onchain payment steps require a to account asset to be resolved
`payments-3031` 400 To account {id} is not a fiat account The to account is not the expected fiat_bank type for offramp payments
`payments-3032` 400 Account {id} does not belong to workspace The referenced account does not belong to the payment's workspace
`payments-3033` 400 Account not found for address {address} No account found with the specified wallet address
`payments-3034` 400 Account {id} has no entity ID The account is missing the required entity ID for payment processing
`payments-3035` 400 Fiat account {id} not found for workspace The specified fiat account was not found in the workspace
`payments-3036` 400 Payment has not been updated with accounts. Please call PATCH /payments/:paymentId first. Payment must be updated with account IDs before execution
`payments-3037` 400 Workspace does not have a Turnkey sub-organization ID configured Workspace is missing Turnkey sub-organization configuration required for signing
`payments-3038` 400 Unsupported network for signing: {network} The payment network is not supported for transaction signing
`payments-3039` 400 Signature is required for payment execution A Turnkey stamp/signature is required to execute the payment
`payments-3040` 400 Cannot execute payment from external account {id} External accounts support CRUD operations only. Use managed accounts for payment execution.
`payments-3041` 400 No payment execution provider available for this payment No supported provider (Alfred, Circle) could be resolved for this payment type and account combination
`payments-3042` 400 Source account asset not found The source account asset could not be found for payment execution
`payments-3043` 400 Source account wallet address not found The source account does not have a crypto wallet address
`payments-3044` 400 Destination account asset not found The destination account asset could not be found for payment execution
`payments-3045` 400 Destination account wallet address not found The destination account does not have a crypto wallet address
`payments-3046` 400 Unsupported currency: {currency} The specified currency is not supported for transaction building
`payments-3047` 400 Wallet address mismatch The provided wallet address does not match the account's wallet address
`payments-3048` 400 Amount mismatch between transfer step and provided amount The provided amount does not match the transfer step amount
`payments-3049` 400 Transfer step missing from account asset ID The transfer step does not have a from account asset ID set
`payments-3050` 400 Transfer step missing to account asset ID The transfer step does not have a to account asset ID set
`payments-3051` 400 Destination account not found for {id} The destination account could not be found by address or ID
## Tenants
Error Code HTTP Status Error Message Description
`tenants-1000` 404 Tenant not found The specified tenant ID does not exist
`tenants-2000` 403 You do not have access to this tenant User tries to access a tenant that belongs to a different workspace
`tenants-3000` 400 business_legal_name is required to create a tenant User creates a tenant without providing the required business_legal_name field
`tenants-3001` 400 business_legal_name is required and must be 1-255 characters User provides a business_legal_name that doesn't meet length requirements
`tenants-3002` 400 business_address_country must be a valid ISO 3166-1 alpha-2 country code User provides a business_address_country that is not a valid 2-letter country code
`tenants-3003` 400 webhook_url must be a valid HTTPS URL User provides a webhook_url that is not a properly formatted HTTPS URL
## Treasury
Error Code HTTP Status Error Message Description
`treasury-1000` 404 Rebalance with id '{id}' not found The specified rebalance ID does not exist
`treasury-1001` 404 Transfer step not found for rebalance with id '{id}' The transfer step associated with the rebalance could not be found
`treasury-1002` 404 Account with id '{id}' not found The specified account ID does not exist or does not belong to this workspace
`treasury-1100` 404 Deposit with id '{id}' not found The specified deposit ID does not exist in this workspace
`treasury-1101` 404 Account with id '{id}' not found The specified account ID does not exist or does not belong to this workspace
`treasury-1102` 404 Source account with id '{id}' not found The source bank account for this deposit could not be found
`treasury-1103` 404 VAN account with id '{id}' not found The virtual account number (VAN) account could not be found
`treasury-2100` 409 Deposit is still being planned. Wait for the deposit.created webhook before fetching instructions. Instructions were requested while the deposit's async planning step is still running (steps array is empty). Retry after the deposit.created webhook fires.
`treasury-3000` 400 Only Circle Mint ledger-to-ledger rebalancing is currently supported Rebalancing is only available between Circle Mint ledger accounts
`treasury-3001` 400 Network must not be specified for ledger accounts Ledger-to-ledger rebalances operate without a blockchain network
`treasury-3002` 400 At least one of from_amount or to_amount must be provided A rebalance requires at least one amount to determine the transfer size
`treasury-3003` 400 Invalid currency pair for rebalance The specified from_currency and to_currency combination is not supported for Circle rebalancing
`treasury-3004` 400 from_amount and to_amount must be equal when currencies are the same When both amounts are provided for same-currency rebalances, they must match
`treasury-3005` 400 Source or destination account is missing Circle wallet ID The account does not have a Circle wallet ID configured, which is required for ledger transfers
`treasury-3006` 400 Transfer step is missing account asset IDs The transfer step does not have the required from/to account asset references
`treasury-3007` 400 Network is required when rebalancing between wallet accounts On-chain wallet-to-wallet rebalances need from_network and to_network to be set
`treasury-3008` 400 from_network and to_network must match for wallet-to-wallet rebalance Cross-chain wallet-to-wallet rebalance is not supported yet; source and destination network must be identical
`treasury-3009` 400 Wallet account is missing a crypto wallet address On-chain wallet-to-wallet rebalance requires both accounts to have a crypto wallet address
`treasury-3010` 400 Wallet-to-wallet rebalance is only supported between managed wallets Both source and destination accounts must be Tesser-managed (is_managed = true) wallet accounts
`treasury-3011` 400 Currency '{currency}' is not supported for wallet-to-wallet rebalance On-chain wallet-to-wallet rebalance currently supports USDC only
`treasury-3012` 400 Network '{network}' is not supported for wallet-to-wallet rebalance On-chain wallet-to-wallet rebalance currently supports BASE and BASE_SEPOLIA only
`treasury-3013` 400 Rebalance step is not in a signable state The step must be in 'created' state with a prepared unsigned transaction to accept a signature
`treasury-3014` 400 Rebalance step is missing an unsigned transaction payload The rebalance has not yet prepared an unsigned transaction — retry in a moment
`treasury-3015` 400 Signature is invalid or could not be verified The Turnkey stamp did not produce a valid signed transaction
`treasury-3016` 400 Signed transaction does not match the rebalance details The destination address or amount in the signed transaction does not match the prepared rebalance step
`treasury-3017` 400 Destination wallet is not registered with Circle or compliance is not active The destination managed wallet must have an active Circle business recipient registration before it can receive rebalance transfers
`treasury-3018` 400 to_network is required when destination is a managed wallet Rebalances to a managed stablecoin wallet must specify the target blockchain network
`treasury-3019` 400 Network '{network}' is not supported in this environment Rebalances to managed wallets only support BASE in production and BASE_SEPOLIA in non-production environments
`treasury-3020` 400 Signature is not a valid base64-encoded {body, stamp} envelope The signature field must be base64-encoded JSON containing the Turnkey activity body and stamp header value
`treasury-3021` 400 Stamped activity type must be '{expected}', received '{actual}' The stamped Turnkey activity must be ACTIVITY_TYPE_SIGN_TRANSACTION_V2 to sign a rebalance step
`treasury-3022` 400 Stamped activity unsigned transaction does not match the rebalance step The unsignedTransaction inside the stamped Turnkey activity must equal the unsigned transaction prepared by the rebalance step
`treasury-3023` 400 Stamped activity signWith address does not match the rebalance source wallet The signWith parameter inside the stamped Turnkey activity must equal the source wallet's on-chain address
`treasury-3024` 400 Stamped activity organizationId does not match the workspace's Turnkey sub-organization The stamped Turnkey activity's organizationId must equal the workspace's configured Turnkey sub-org ID
`treasury-3025` 400 Turnkey could not produce a signed transaction Turnkey rejected or failed the stamped sign_transaction activity. Verify the stamp matches the body bytes, the API key is registered in the sub-org, and the wallet exists in that sub-org.
`treasury-3026` 400 Stamped activity parameters.type must be '{expected}', received '{actual}' The Turnkey transaction type inside parameters.type must match the network of the rebalance step (e.g. TRANSACTION_TYPE_ETHEREUM for BASE)
`treasury-3100` 400 Account '{id}' is not a ledger account Deposits can only be made to ledger accounts
`treasury-3101` 400 Account '{id}' is not configured for deposits. No supported provider found The destination account does not have a supported deposit provider configured
`treasury-3102` 400 Ledger '{id}' is missing Circle Mint metadata The ledger account does not have the required Circle Mint configuration
`treasury-3103` 400 Circle compliance not yet accepted for ledger. Cannot create deposit Circle compliance must be accepted before deposits can be created
`treasury-3104` 400 Source account '{id}' does not match the configured source bank for this ledger The provided source account does not match the bank account configured for this ledger
`treasury-3105` 400 Source bank account '{id}' not found or is not a fiat bank account The source bank account could not be found or is not the correct account type
`treasury-3106` 400 Ledger account '{id}' does not have a '{currency}' asset The destination ledger account does not hold the requested currency
`treasury-3107` 400 Invalid currency combination for Circle deposit: {fromCurrency} → {toCurrency} The specified from_currency and to_currency combination is not supported for Circle deposits
`treasury-3108` 400 Deposit '{id}' is already finalized. Wire instructions are no longer available The deposit has already been finalized and instructions cannot be retrieved
`treasury-3109` 400 Deposit '{id}' has no transfer step The deposit does not have the expected transfer step
`treasury-3110` 400 Deposit '{id}' transfer step has no source account The deposit transfer step is missing its source account reference
`treasury-3111` 400 Deposit '{id}' transfer step has no destination account The deposit transfer step is missing its destination account reference
`treasury-3112` 400 Source account '{id}' is missing required bank details The source account does not have all required bank details (bank name, code type, identifier code, or account number)
`treasury-3113` 400 VAN account '{id}' missing virtual_account_number metadata The VAN account does not have the required virtual account number metadata
`treasury-3114` 400 VAN account '{id}' is missing required field: {field} The VAN account is missing a required field for wire instructions (e.g., bank name, account number, SWIFT code, beneficiary name)
`treasury-3115` 400 Simulated deposits are not available in production Deposit simulation is only available in sandbox environments
`treasury-3116` 400 to_network is required when destination is a managed wallet Deposits to a managed stablecoin wallet must specify the target blockchain network
`treasury-3117` 400 to_network must not be specified for ledger accounts Deposits to ledger accounts operate without a blockchain network
`treasury-3118` 400 Network '{network}' is not supported in this environment Deposits to managed wallets only support BASE in production and BASE_SEPOLIA in non-production environments
`treasury-3119` 400 Deposit '{id}' has no estimated source amount yet The deposit's estimated.from.amount or estimated.from.currency is not populated. The estimate is filled in after planning; this can occur if instructions are requested before the deposit has been planned.
`treasury-3120` 400 No deposit provider can handle this configuration. Verify the source bank account, destination account compliance state, and currency pair. getEligibleDepositProviders returned an empty array — no provider matches the deposit's source account, destination account, currency pair, or workspace provider configuration (e.g., missing Circle API key in the vault).
`treasury-3133` 400 Ledger '{id}' is missing OpenFX metadata The destination ledger account does not have OpenFX configuration (metadata.openfx)
`treasury-3134` 400 Currency pair {fromCurrency} → {toCurrency} is not supported for OpenFX deposits OpenFX deposits support same-currency fiat→fiat or fiat→stablecoin (on-ramp) pairs only
`treasury-3135` 400 Source bank currency '{currency}' does not match deposit from_currency The OpenFX-registered source bank account is configured for a different currency than the deposit request
`treasury-3139` 400 Wallet address '{address}' is not registered in OpenFX for {coin} on {network} The wallet must be manually registered in the OpenFX dashboard before it can receive deposits
`treasury-3140` 400 Multiple OpenFX withdrawal addresses matched ({count}) — registration is ambiguous More than one verified active withdrawal address matched; resolve via the OpenFX dashboard
`treasury-3141` 400 OpenFX coinNetworkName '{coinNetworkName}' is not a known Tesser network The network returned by OpenFX does not map to a supported Tesser network identifier
`treasury-3142` 400 to_network is required for OpenFX wallet deposits OpenFX wallet deposits must specify the target blockchain network via to_network
`treasury-3200` 400 from_currency must be a valid currency code User provides an invalid from_currency value
`treasury-3201` 400 to_currency must be a valid currency code User provides an invalid to_currency value
`treasury-3202` 400 from_amount must be a valid decimal string User provides a from_amount that is not a valid number
`treasury-3203` 400 to_amount must be a valid decimal string User provides a to_amount that is not a valid number
`treasury-3204` 400 from_account_id must be a valid UUID User provides a from_account_id that is not a valid UUID
`treasury-3205` 400 to_account_id must be a valid UUID User provides a to_account_id that is not a valid UUID
`treasury-3206` 400 from_network must be a valid network identifier User provides an invalid from_network value
`treasury-3207` 400 to_network must be a valid network identifier User provides an invalid to_network value
`treasury-5100` 502 Failed to obtain OpenFX quote for {fromCurrency} → {toCurrency} OpenFX rejected or did not respond to the projection quote during deposit planning. The deposit cannot be created until OpenFX returns a valid rate.
`treasury-5101` 502 OpenFX trade failed for deposit '{id}' after retry budget The swap leg of an OpenFX cross-currency deposit could not be executed within the retry budget. The swap step has been marked as failed.
`treasury-5102` 502 OpenFX quote expired before trade could execute OpenFX rejected the trade because the quote had expired (HTTP 422). The next attempt will request a fresh quote.
`treasury-5103` 502 OpenFX withdrawal failed for deposit '{id}' (reason: {reason}) The wallet leg of an OpenFX deposit could not be submitted to OpenFX. The withdrawal step has been marked as failed.
## Vault
Error Code HTTP Status Error Message Description
`vault-0001` 500 Failed to store sensitive data in vault The vault provider failed to store the token. This may be due to a temporary outage or configuration issue.
`vault-0002` 500 Vault provider did not return a token ID The vault provider accepted the data but did not return a token ID. This indicates an unexpected API response.
`vault-0003` 400 Unknown vault field The specified field name is not configured for vault storage. Check the VaultFieldConfig for supported fields.
`vault-0004` 400 Invalid field type for operation The operation does not match the field type. For example, trying to store text in a file-only field.
`vault-0005` 409 Vault record already exists for this resource and field A create-only vault write found an existing record for the same (resourceId, fieldName). Rotate or delete the existing record instead.
`vault-0010` 404 Vault token not found The specified token does not exist in the vault. It may have been deleted or never created.
`vault-0011` 500 Failed to retrieve token from vault The vault provider failed to retrieve the token. This may be due to a temporary outage or authentication issue.
`vault-0012` 500 Failed to reveal token value The vault provider failed to reveal the token value. The API key may not have sufficient permissions.
`vault-0020` 500 Failed to delete token from vault The vault provider failed to delete the token. This may be due to a temporary outage or authentication issue.
`vault-0030` 502 Vault proxy request failed The vault proxy failed to forward the request to the destination. Check the destination URL and authentication.
`vault-0031` 502 Vault proxy destination returned an error The vault proxy successfully forwarded the request, but the destination returned an error response.
`vault-0040` 503 Vault provider is not configured The vault provider API key is not configured. Contact support to enable vault functionality.
`vault-0041` 400 Liquidation provider not found The specified liquidation provider is not configured. Check the available providers.
`vault-0050` 502 Failed to forward vaulted data to provider The vaulted data could not be forwarded to the liquidation provider. This may be due to a provider outage or misconfiguration.
`vault-0051` 422 Provider rejected the forwarded data The liquidation provider rejected the forwarded data. Check the data format and provider requirements.
## Circle
Error Code HTTP Status Error Message Description
`circle-1000` 404 Circle API key not configured for organization The organization does not have a Circle Mint API key configured in the vault
`circle-2000` 401 Circle API key authentication failed The configured Circle Mint API key failed authentication with Circle API
`circle-4290` 429 Cannot complete request because rate limited by provider Circle Mint The Circle Mint API returned a 429 rate limit response
`circle-5000` 502 Circle Mint service error An error occurred while communicating with the Circle Mint API
`circle-5001` 503 Circle Mint service temporarily unavailable The Circle Mint API is temporarily unavailable
## Idempotency
Error Code HTTP Status Error Message Description
`idempotency-0001` 409 A request with this idempotency key is currently being processed. Please wait and retry. Another request with the same idempotency key is still in progress
`idempotency-0002` 400 Keys for idempotent requests can only be used with the same parameters they were first used with. The idempotency key was previously used with a different request body. Use a different key for different requests.
--- ## Document: Deposits and Withdrawals On-ramping and off-ramping operations URL: /overviews/deposits-and-withdrawals # Deposits and Withdrawals Deposits and Withdrawals involve funds exchange with a liquidity provider to on-ramp funds into your organization’s wallets or off-ramp funds back into external bank accounts. - **On-ramping:** Exchange fiat for stablecoin(s) via a stablecoin liquidity provider. Fiat funds leave your organization’s, or your customers’, fiat bank account(s) and stablecoins are deposited to your wallets. - **Off-ramping:** Exchange stablecoin(s) for fiat via a liquidity provider. Stablecoins leave your organization’s, or your customers’, wallets and fiat is deposited to fiat bank account(s). --- ## Document: Currencies Supported fiat and crypto currencies URL: /overviews/currencies # Currencies import { Badge } from "zudoku/ui/Badge"; import { Callout } from "zudoku/ui/Callout"; ## Currencies The Currencies resource lists all fiat and digital assets supported by the platform. ### Overview You can retrieve a comprehensive list of supported currencies, including details about their decimals and associated blockchain networks (for crypto assets). Always pay attention to the `decimals` field when handling amounts to ensure accurate calculations. ### Currency Types - Fiat Traditional currencies like USD. - Stablecoins Digital assets pegged to fiat currencies (e.g., USDC, USDT). ### Data Model Each currency object includes: - `key`: The currency symbol (e.g., `USD`, `USDC`). - `name`: Display name (e.g., `US Dollar`, `Circle USD`). - `decimals`: Precision of the currency. - `network`: The blockchain network (null for fiat). See the [API Reference](/api#tag/Currencies) for the full list. --- ## Document: Counterparties Manage external individuals and businesses URL: /overviews/counterparties # Counterparties A counterparty represents an originator (ultimate sender) or beneficiary (ultimate receiver) of a payment. Counterparties can be individuals or businesses. Counterparties are associated to Accounts and belong to a Workspace. Registering counterparties in advance of submitting payments enables OFAC (sanctions) screening and helps meet regulatory requirements. To comply with Travel Rule obligations, counterparty information will be recorded for stablecoin payouts and be included in payment instructions sent to fiat off-ramps. \*Note: Tesser does not conduct KYC or KYB verification on counterparties. Organizations must have completed KYC/KYB verification of originator counterparties prior to registering them with the Tesser platform. To meet regulatory obligations and maintain consistency of data requirements, Tesser requires the following fields to be supplied about counterparties for registration: **Required Fields** - Legal name (first name and last name) - Physical address (legal street address, no P.O. Boxes or virtual addresses) **Optional Fields** - Date of birth - National identification number (Passport, ID card number, Tax Identification number, Social Security Number) **Required Fields** - Legal entity name - Physical address (legal street address, no P.O. Boxes or virtual addresses) - Legal entity identifier. Legal entity identifier may be a registration number or tax ID. \*Required only for counterparties who will be originators of fiat payouts. **Optional Fields** - Doing business as (DBA) or Trade name - Legal entity identifier (for beneficiaries) > If you did not supply required data during registration that is required in order to send a payment, you will receive an error indicating the missing data. You can update the counterparty record and then retry the payment. --- ## Document: Compliance and Risk Management Comprehensive compliance management to meet regulatory requirements URL: /overviews/compliance-and-risk # Compliance and Risk Management Tesser provides comprehensive compliance management to meet regulatory requirements for financial institutions. ## Pre-transaction screening **All wallets in the payout flow are screened pre-transaction, including risk categorization and scoring:** - For stablecoin payouts, the beneficiary's wallet is automatically screened. - For fiat payouts, risk assessment is performed on any intermediary wallets. - If your customers settle with you in stablecoins to fund payouts, wallets that customers send from can be screened as well. **Wallets are screened on 152 factors across 4 categories:** - **Ownership.** The wallet is directly associated with or controlled by entities engaged in risky activities. - **Counterparty.** The wallet has directly transacted with entities or addresses flagged for risky behavior. - **Behavioral.** The wallet exhibits transaction patterns or behaviors commonly associated with illicit activity or obfuscation techniques. - **Indirect.** The wallet has transacted with another address that is connected to risky activity. The activity of wallets up to 20 degrees removed is assessed as part of indirect exposure calculations. See the full list of [risk factors for a wallet](#wallet-risk-categories) below. **Transaction patterns for the originator and beneficiary are assessed:** This includes total volumes sent and received, frequency and velocity of activity, suspicious amounts, etc., as well as the specific interaction pattern between the originator and beneficiary. **Risk Scores and Outcomes:** Each payment is assessed for multiple risk factors, each scored from Low to Severe. The most severe detected risk factor, along with your compliance policy, determines whether the payment is approved, approved with a flag, requires review, or rejected. - **Low or Medium Risk**: Automatic approval - **High Risk**: Requires manual review by your organization - **Severe Risk**: Automatically rejected **Risk Profiles** Tesser offers three pre-configured risk profiles to match your organization's risk appetite: 1. **Conservative**: Lower risk thresholds, more transactions require manual review 2. **Balanced**: Moderate risk tolerance with selective manual review 3. **Permissive**: Higher risk tolerance, fewer manual reviews required Each profile defines how the platform responds to different risk categories. Need a custom risk profile? Contact us to configure specific thresholds and actions tailored to your requirements. **[Optional] Counterparties can be sanctions screened.** In addition to any screening an organization conducts, Tesser can perform OFAC, PEP, and adverse media screening of beneficiaries and their fiat accounts prior to payment initiation. Contact us if you are interested in having Tesser perform sanctions screening. ## Manual Review Process Payments requiring manual review can be processed through the compliance review endpoint (`/payments/{paymentId}/review`): ```bash curl -X POST https://api.tesser.xyz/v1/payments/{paymentId}/review \ -H "Authorization: Bearer your-access-token" \ -H "Content-Type: application/json" \ -d '{ "is_approved": true }' ``` The response includes the timestamp and final decision. Payments can also be reviewed and processed in the Tesser dashboard. ## On-going Monitoring Even after a transaction is complete, beneficiary wallets are monitored to track any changes in risk factors about the wallet. ## Travel Rule Tesser handles travel rule requirements automatically as you use the platform: - No special actions are required to be compliant — the platform ensures required originator and beneficiary data is attached to payments where applicable. - During counterparty and account creation, we collect the information needed to support compliant payments to/from that account (jurisdiction- and method-specific). - In rare cases, additional KYB details may be requested at payment intent or execution time to satisfy provider- or corridor-specific rules. Account information is collected and transmitted as part of Travel Rule compliance for stablecoin payouts or fiat payouts: | **Information transmitted** | **Required for Originator** | **Required for Beneficiary** | **Notes** | | --- | --- | --- | --- | | **Legal Name** | Individual's legal first and last name, or Business's legal entity name | Individual's legal first and last name, or Business's legal entity name | | | **Physical Address** | Legal address of the individual or business. (Street address, City, State/District/Region, Postal Code, Country) | Legal address of the individual or business. (Street address, City, State/District/Region, Postal Code, Country) | No P.O. Boxes or virtual addresses. | | **Legal Entity Identifier** | Required for business originators of fiat payouts. Not required for individuals. | Not required for beneficiaries | Legal entity Identifier may be a registration number or tax ID. | | **Account Identifier*** | Account Number or IBAN. | Account Number, IBAN, or Wallet Address. | For cash-funded remittance transaction, will be a unique identifier for the originator or transaction | | **Financial Institution (FI) info** | Name and Identifier of the FI | Name and Identifier of the FI (For fiat payouts only) | | > *For Travel Rule purposes, information about the originator's fiat account is typically transmitted to beneficiary financial institutions, unless wallets are provisioned for *and* self-custodied by the originator. ## Wallet Risk Categories | **Type of risk** | **Category** | **Description** | | --- | --- | --- | | Behavioral | Binary Tree | Part of a Binary Tree transaction | | Behavioral | CoinJoin Transactions | Involved in a CoinJoin-like transaction | | Behavioral | Extortion with Common Destination | Part of an Extortion with Common Destination transaction | | Behavioral | Graphton | Part of a unique "trail" of transactions | | Behavioral | Mass Registration | Involved in Mass Registration | | Behavioral | Peeling Chain | Involved in a Peeling Chain | | Behavioral | Peeling Chain with Common Destination | Part of a Peeling Chain with Common Destination transaction | | Behavioral | Web Flow | Part of a Web Flow transaction | | Counterparty | Adult Content | Address previously transacted with Adult Content | | Counterparty | Banned or Controlled Substances | Address previously transacted with Banned or Controlled Substances | | Counterparty | Blocklisted | Address previously transacted with Blocklisted | | Counterparty | Carding / PII Shop | Address previously transacted with Carding / PII Shop | | Counterparty | Cash-to-Crypto | Address previously transacted with Cash-to-Crypto | | Counterparty | Child Sexual Abuse Material (CSAM) | Address previously transacted with Child Sexual Abuse Material (CSAM) | | Counterparty | Child Sexual Abuse Material (CSAM) Consumer | Address previously transacted with Child Sexual Abuse Material (CSAM) Consumer | | Counterparty | Child Sexual Abuse Material (CSAM) Scam | Address previously transacted with Child Sexual Abuse Material (CSAM) Scam | | Counterparty | Child Sexual Abuse Material (CSAM) Vendor | Address previously transacted with Child Sexual Abuse Material (CSAM) Vendor | | Counterparty | Community Complaint | Address previously transacted with Community Complaint | | Counterparty | Counterfeit Goods | Address previously transacted with Counterfeit Goods | | Counterparty | Cybercrime Services | Address previously transacted with Cybercrime Services | | Counterparty | Darknet Market | Address previously transacted with Darknet Market | | Counterparty | Decentralized Exchange | Address previously transacted with Decentralized Exchange | | Counterparty | Decentralized File Sharing Service | Address previously transacted with Decentralized File Sharing Service | | Counterparty | Decentralized Gambling | Address previously transacted with Decentralized Gambling Service | | Counterparty | Decentralized Investment Fraud | Address previously transacted with Decentralized Investment Fraud | | Counterparty | Decentralized Investment Scheme | Address previously transacted with Decentralized Investment Scheme | | Counterparty | Decentralized Marketplace | Address previously transacted with Decentralized Marketplace | | Counterparty | Escort Service / Prostitution | Address previously transacted with Escort Service / Prostitution | | Counterparty | Extortion / Blackmail | Address previously transacted with Extortion / Blackmail | | Counterparty | Gambling Service | Address previously transacted with Gambling Service | | Counterparty | Hacked or Exploited Funds | Address previously transacted with Hacked or Exploited Funds | | Counterparty | High-Risk Exchange | Address previously transacted with High-Risk Exchange | | Counterparty | Human Trafficking | Address previously transacted with Human Trafficking | | Counterparty | Illicit Goods and Services | Address previously transacted with Illicit Goods and Services | | Counterparty | Imposter Site or Service | Address previously transacted with Imposter Site or Service | | Counterparty | Investment Fraud | Address previously transacted with Investment Fraud | | Counterparty | Investment Scheme | Address previously transacted with Investment Scheme | | Counterparty | Known Hacker Group | Address previously transacted with Known Hacker Group | | Counterparty | Lending Service | Address previously transacted with Lending Service | | Counterparty | Malware | Address previously transacted with Malware | | Counterparty | Mining | Address previously transacted with Mining | | Counterparty | Mixer | Address previously transacted with Mixer | | Counterparty | Non-Custodial Exchange | Address previously transacted with Non-Custodial Exchange | | Counterparty | Online Username Reselling | Address previously transacted with Online Username Reselling | | Counterparty | P2P Crypto Marketplace | Address previously transacted with P2P Crypto Marketplace | | Counterparty | Piracy | Address previously transacted with Piracy | | Counterparty | Politically Exposed Person | Address previously transacted with Politically Exposed Person | | Counterparty | Ponzi Scheme | Address previously transacted with Ponzi Scheme | | Counterparty | Ransomware | Address previously transacted with Ransomware | | Counterparty | Sanctions | Address previously transacted with Sanctions | | Counterparty | Scam | Address previously transacted with Scam | | Counterparty | Sexual Exploitation | Address previously transacted with Sexual Exploitation | | Counterparty | Special Measures | Address previously transacted with Special Measures | | Counterparty | Terrorist Financing | Address previously transacted with Terrorist Financing | | Counterparty | Trusted Community Complaint | Address associated with Trusted Community Complaint | | Counterparty | Violent Extremism | Address previously transacted with Violent Extremism | | Indirect | Adult Content | Address has indirect exposure to Adult Content | | Indirect | Banned or Controlled Substances | Address has indirect exposure to Banned or Controlled Substances | | Indirect | Blocklisted | Address has indirect exposure to Blocklisted | | Indirect | Carding / PII Shop | Address has indirect exposure to Carding / PII Shop | | Indirect | Cash-to-Crypto | Address has indirect exposure to Cash-to-Crypto | | Indirect | Child Sexual Abuse Material (CSAM) | Address has indirect exposure to Child Sexual Abuse Material (CSAM) | | Indirect | Child Sexual Abuse Material (CSAM) Consumer | Address has indirect exposure to Child Sexual Abuse Material (CSAM) Consumer | | Indirect | Child Sexual Abuse Material (CSAM) Scam | Address has indirect exposure to Child Sexual Abuse Material (CSAM) Scam | | Indirect | Child Sexual Abuse Material (CSAM) Vendor | Address has indirect exposure to Child Sexual Abuse Material (CSAM) Vendor | | Indirect | Community Complaint | Address has indirect exposure to Community Complaint | | Indirect | Counterfeit Goods | Address has indirect exposure to Counterfeit Goods | | Indirect | Cybercrime Services | Address has indirect exposure to Cybercrime Services | | Indirect | Darknet Market | Address has indirect exposure to Darknet Market | | Indirect | Decentralized Exchange | Address has indirect exposure to Decentralized Exchange | | Indirect | Decentralized File Sharing Service | Address has indirect exposure to Decentralized File Sharing Service | | Indirect | Decentralized Gambling | Address has indirect exposure to Decentralized Gambling | | Indirect | Decentralized Investment Fraud | Address has indirect exposure to Decentralized Investment Fraud | | Indirect | Decentralized Investment Scheme | Address has indirect exposure to Decentralized Investment Scheme | | Indirect | Decentralized Marketplace | Address has indirect exposure to Decentralized Marketplace | | Indirect | Escort Service / Prostitution | Address has indirect exposure to Escort Service / Prostitution | | Indirect | Extortion / Blackmail | Address has indirect exposure to Extortion / Blackmail | | Indirect | Gambling Service | Address has indirect exposure to Gambling Service | | Indirect | Hacked or Exploited Funds | Address has indirect exposure to Hacked or Exploited Funds | | Indirect | High-Risk Exchange | Address has indirect exposure to High-Risk Exchange | | Indirect | Human Trafficking | Address has indirect exposure to Human Trafficking | | Indirect | Illicit Goods and Services | Address has indirect exposure to Illicit Goods and Services | | Indirect | Imposter Site or Service | Address has indirect exposure to Imposter Site or Service | | Indirect | Investment Fraud | Address has indirect exposure to Investment Fraud | | Indirect | Investment Scheme | Address has indirect exposure to Investment Scheme | | Indirect | Known Hacker Group | Address has indirect exposure to Known Hacker Group | | Indirect | Lending Service | Address has indirect exposure to Lending Service | | Indirect | Malware | Address has indirect exposure to Malware | | Indirect | Mining | Address has indirect exposure to Mining | | Indirect | Mixer | Address has indirect exposure to Mixer | | Indirect | Non-Custodial Exchange | Address has indirect exposure to Non-Custodial Exchange | | Indirect | Online Username Reselling | Address has indirect exposure to Online Username Reselling | | Indirect | P2P Crypto Marketplace | Address has indirect exposure to P2P Crypto Marketplace | | Indirect | Piracy | Address has indirect exposure to Piracy | | Indirect | Politically Exposed Person | Address has indirect exposure to Politically Exposed Person | | Indirect | Ponzi Scheme | Address has indirect exposure to Ponzi Scheme | | Indirect | Ransomware | Address has indirect exposure to Ransomware | | Indirect | Sanctions | Address has indirect exposure to Sanctions | | Indirect | Scam | Address has indirect exposure to Scam | | Indirect | Sexual Exploitation | Address has indirect exposure to Sexual Exploitation | | Indirect | Special Measures | Address has indirect exposure to Special Measures | | Indirect | Terrorist Financing | Address has indirect exposure to Terrorist Financing | | Indirect | Trusted Community Complaint | Address associated with Trusted Community Complaint | | Indirect | Violent Extremism | Address has indirect exposure to Violent Extremism | | Ownership | Adult Content | Address associated with Adult Content | | Ownership | Banned or Controlled Substances | Address associated with Banned or Controlled Substances | | Ownership | Blocklisted | Address associated with Blocklisted | | Ownership | Carding / PII Shop | Address associated with Carding / PII Shop | | Ownership | Cash-to-Crypto | Address associated with Cash-to-Crypto | | Ownership | Child Sexual Abuse Material (CSAM) | Address associated with Child Sexual Abuse Material (CSAM) | | Ownership | Child Sexual Abuse Material (CSAM) Consumer | Address associated with Child Sexual Abuse Material (CSAM) Consumer | | Ownership | Child Sexual Abuse Material (CSAM) Scam | Address associated with Child Sexual Abuse Material (CSAM) Scam | | Ownership | Child Sexual Abuse Material (CSAM) Vendor | Address associated with Child Sexual Abuse Material (CSAM) Vendor | | Ownership | Community Complaint | Address associated with Community Complaint | | Ownership | Counterfeit Goods | Address associated with Counterfeit Goods | | Ownership | Cybercrime Services | Address associated with Cybercrime Services | | Ownership | Darknet Market | Address associated with Darknet Market | | Ownership | Decentralized Exchange | Address associated with Decentralized Exchange | | Ownership | Decentralized File Sharing Service | Address associated with Decentralized File Sharing Service | | Ownership | Decentralized Gambling | Address associated with Decentralized Gambling Service | | Ownership | Decentralized Investment Fraud | Address associated with Decentralized Investment Fraud | | Ownership | Decentralized Investment Scheme | Address associated with Decentralized Investment Scheme | | Ownership | Decentralized Marketplace | Address associated with Decentralized Marketplace | | Ownership | Escort Service / Prostitution | Address associated with Escort Service / Prostitution | | Ownership | Extortion / Blackmail | Address associated with Extortion / Blackmail | | Ownership | Gambling Service | Address associated with Gambling Service | | Ownership | Hacked or Exploited Funds | Address associated with Hacked or Exploited Funds | | Ownership | High-Risk Exchange | Address associated with High-Risk Exchange | | Ownership | Human Trafficking | Address associated with Human Trafficking | | Ownership | Illicit Goods and Services | Address associated with Illicit Goods and Services | | Ownership | Imposter Site or Service | Address associated with Imposter Site or Service | | Ownership | Investment Fraud | Address associated with Investment Fraud | | Ownership | Investment Scheme | Address associated with Investment Scheme | | Ownership | Known Hacker Group | Address associated with Known Hacker Group | | Ownership | Lending Service | Address associated with Lending Service | | Ownership | Malware | Address associated with Malware | | Ownership | Mining | Address associated with Mining | | Ownership | Mixer | Address associated with Mixer | | Ownership | Non-Custodial Exchange | Address associated with Non-Custodial Exchange | | Ownership | Online Username Reselling | Address associated with Online Username Reselling | | Ownership | P2P Crypto Marketplace | Address associated with P2P Crypto Marketplace | | Ownership | Piracy | Address associated with Piracy | | Ownership | Politically Exposed Person | Address associated with Politically Exposed Person | | Ownership | Ponzi Scheme | Address associated with Ponzi Scheme | | Ownership | Ransomware | Address associated with Ransomware | | Ownership | Sanctions | Address associated with Sanctions | | Ownership | Scam | Address associated with Scam | | Ownership | Sexual Exploitation | Address associated with Sexual Exploitation | | Ownership | Special Measures | Address associated with Special Measures | | Ownership | Terrorist Financing | Address associated with Terrorist Financing | | Ownership | Trusted Community Complaint | Address associated with Trusted Community Complaint | | Ownership | Violent Extremism | Address associated with Violent Extremism | --- ## Document: Authentication How to authenticate with the Tesser API URL: /overviews/authentication # Authentication All requests to the Tesser API must include a valid access token. Tokens are generated using your API credentials, which are provisioned in the [Tesser Dashboard](https://app.tesser.xyz). ## Credentials Each workspace has a **Client ID** and **Client Secret** pair. You can find these in the Tesser Dashboard under **Settings > API Keys**. Keep your client secret secure and never expose it in client-side code or public repositories. ## Generate an API Token To obtain an access token, send a `POST` request to the Tesser token endpoint with your credentials: ```bash curl --request POST \ --url https://auth.tesser.xyz/oauth/token \ --header 'Content-Type: application/json' \ --data '{ "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "audience": "https://api.tesser.xyz", "grant_type": "client_credentials" }' ``` ### Token Response A successful request returns a JSON response containing your access token: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...", "token_type": "Bearer", "expires_in": 86400 } ``` | Field | Description | | --- | --- | | `access_token` | The token to include in API requests | | `token_type` | Always `Bearer` | | `expires_in` | Token lifetime in seconds (default: 86400 = 24 hours) | ## Using the Token Include the access token in the `Authorization` header of every API request: ```bash curl --request GET \ --url https://api.tesser.xyz/counterparties \ --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...' ``` ## Token Expiration Tokens expire after the duration specified in `expires_in` (default 24 hours). When a token expires, the API returns a `401 Unauthorized` response. To maintain uninterrupted access: - Request a new token before the current one expires. - Do not cache tokens indefinitely. - There are no refresh tokens. Simply request a new token using the same credentials. --- ## Document: Accounts Manage fiat bank accounts and crypto wallets URL: /overviews/accounts # Accounts An account represents a store of value for fiat currencies and/or stablecoins. - **Bank account**: Account that exists at a regulated depository financial institution (i.e. a bank). Stores fiat. - **Wallet**: An on-chain wallet. Stores stablecoins. - **Ledger**: An account at a liquidity provider/custodian. May store fiat and/or stablecoins. Accounts may be managed by Tesser or unmanaged. "Unmanaged" accounts are accounts that were not provisioned by Tesser, and instead were provisioned by a third party. "Managed" accounts are accounts that are provisioned by Tesser. Accounts will always belong to the workspace they are created in. They can optionally also belong to one of a counterparty or tenant.
*Belongs to:* **Type: Bank account** **Type: Wallet** **Type: Ledger**
**Workspace** n/a

An omnibus wallet an organization uses to manage its treasury operations in an aggregated manner.

(Managed)

An omnibus wallet an organization uses to manage its treasury operations in an aggregated manner.

(Managed)

**Tenant** n/a

A wallet designated for funds belonging to a tenant.

(Managed)

A wallet designated for funds belonging to a tenant.

(Managed)

**Counterparty**

A bank account that belongs to an originator or a beneficiary.

(Unmanaged)

For fiat payouts, bank account info as transmitted as part of Travel Rule compliance.

  • Bank accounts of originators are the ultimate source of funds.
  • Bank accounts of beneficiaries are the ultimate destination of funds.

A wallet designated for an originator or beneficiary provisioned by Tesser. (Managed)

A wallet provisioned for the originator or beneficiary by a non-Tesser provider. (Unmanaged)

A wallet designated for an originator or beneficiary.

(Managed)

--- ## Document: Withdraw Funds via a Liquidity Provider Move funds out of your managed accounts at a liquidity provider to your external bank account. URL: /how-tos/withdraw-funds-via-a-liquidity-provider # Withdraw Funds via a Liquidity Provider Withdrawals via a liquidity provider move funds out of your managed accounts to your external bank account. Before creating a withdrawal, review the [Funds Movement Lifecycle and Data Model](/overviews/funds-movement-lifecycle-and-data-model) overview to understand the shared data shape, lifecycle phases, and statuses that apply to withdrawals and Tesser's other funds-movement resources. ## Prerequisites Liquidity providers require funds movement to and from their platforms to be "first-party" only. For a withdrawal, the destination bank account must be registered both with Tesser and with the liquidity provider executing the withdrawal. Where supported, Tesser will register the destination at your enrolled liquidity provider on your behalf. This mirrors how deposits work, except funds flow toward the fiat bank account rather than from it. For more information on account creation, see [Create an account](/how-tos/create-an-account). The `desired.from.account_id` must be one of your managed accounts at a liquidity provider (for example, an OpenFX USD ledger or a Circle USDC ledger) or a self-custodial wallet. To send funds to a third party, see [Send a Stablecoin Payout](/how-tos/send-a-stablecoin-payout/create-a-stablecoin-payout). ## Withdrawal Workflow A withdrawal executes over one or more steps. - A `transfer` step moves funds from one account to another — for example, a USD push from a provider ledger to your external bank account. - A `swap` step exchanges currencies within the same account (the step's `estimated.from.account_id` and `estimated.to.account_id` are equal). Cross-currency withdrawals include a swap step at the liquidity provider's ledger; for self-custodial wallet sources, the swap is preceded by an on-chain transfer that delivers funds to the ledger. Each step has a status. See [Step Statuses](/overviews/funds-movement-lifecycle-and-data-model#step-statuses) for the full status taxonomy. Tesser plans the sequence of steps during the [Planning](/overviews/funds-movement-lifecycle-and-data-model#planning) phase, executes them during the [Execution](/overviews/funds-movement-lifecycle-and-data-model#execution) phase, and populates the top-level `actual.*` overlay when the withdrawal reaches its [Terminal State](/overviews/funds-movement-lifecycle-and-data-model#terminal-state-and-divergence). ## Exchange Rates for Off-Ramping When a withdrawal includes a swap step that crosses currencies (e.g., USDC redeemed to USD at Circle before the USD push to your bank), the exchange rate depends on the liquidity provider executing the off-ramp. As with on-ramping, 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 sell and buy currencies fluctuates; current rates can be guaranteed for a period of time | | Exchange | Kraken | Exchange rate fluctuates; no guaranteed rate | ## Reserving Funds from the Balance Withdrawals debit funds from a managed account, so Tesser performs a balance check on the `desired.from.account_id` before executing any steps. The withdrawal's top-level `balance_status` field reports the result and `balance_reserved_at` is populated when funds are reserved. See [Balance Statuses](/overviews/funds-movement-lifecycle-and-data-model#balance-statuses) for the full taxonomy. :::note If the `desired.from.account_id` does not have sufficient funds at creation time, the withdrawal's `balance_status` is set to `awaiting_funds` and Tesser republishes the `withdrawal.balance_updated` webhook. The withdrawal remains in `awaiting_funds` until funds arrive or the withdrawal expires. ::: ## Withdrawal Creation Submit a request to [`POST /v1/treasury/withdrawals`](/api/treasury#create-withdrawal). - If applicable, you should specify on which `tenant`'s behalf you are requesting the withdrawal. - For the withdrawal, populate the following fields in the `desired` object: - `desired.from.account_id`: The identifier of one of your managed accounts at a liquidity provider — for example, an OpenFX USDT ledger or a Circle USDC ledger — or a self-custodial wallet. - `desired.from.amount`: Amount to withdraw, in `desired.from.currency`. - `desired.from.currency`: Currency held at `desired.from.account_id`. - `desired.from.network`: Only specified when `desired.from.account_id` is a self-custodial wallet (e.g., `BASE`). - `desired.to.account_id`: Identifier of your external destination — the bank account registered with Tesser ahead of time. - `desired.to.currency`: Currency to deliver to the destination (e.g., `USD` when off-ramping a stablecoin into a USD bank account). *Note: `desired.to.amount` is not auto-populated. The withdrawal'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:** 1. Scenario 1 — Withdrawing USDT from an OpenFX ledger to your external USD bank account (cross-currency off-ramp at OpenFX; swap step + transfer step). 2. Scenario 2 — Withdrawing USDC from a Circle ledger to your external USD bank account (cross-currency off-ramp via Circle redemption; swap step + transfer step). 3. Scenario 3 — Withdrawing USDC from a self-custodial wallet on BASE to your external USD bank account (on-chain transfer to the OpenFX ledger, swap to USD at OpenFX, then USD push to bank; 3 steps total, including wallet signing on the on-chain step). Example request (USDT from an OpenFX ledger off-ramped to your external USD bank account): ```json { "tenant_id": null, "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "currency": "USD" } } } ``` Example request (USDC from a Circle ledger off-ramped via Circle redemption to your external USD bank account): ```json { "tenant_id": null, "desired": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "currency": "USD" } } } ``` Example request (USDC on BASE from a self-custodial wallet, off-ramped via OpenFX to your external USD bank account): ```json { "tenant_id": null, "desired": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "currency": "USD" } } } ``` In the API response, Tesser will create and return an `id` for the withdrawal. The top-level `balance_status` is initially `unreserved` — Tesser performs the balance check asynchronously and publishes the outcome via a `withdrawal.balance_updated` webhook (see [Balance Check](#balance-check-withdrawal-balance-updated) below). The `estimated` and `actual` overlays are all-null at creation; `estimated` is populated when `withdrawal.quote_created` fires, and `actual` is populated when the withdrawal reaches its terminal state. Example response (withdraw USDT from an OpenFX ledger off-ramped to your external USD bank account): ```json { "data": { "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example response (withdraw USDC from a Circle ledger off-ramped via Circle redemption to your external USD bank account): ```json { "data": { "id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example response (withdraw USDC on BASE from a self-custodial wallet, off-ramped via OpenFX to your external USD bank account): ```json { "data": { "id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` ## Withdrawal Quote Created (`withdrawal.quote_created`) After your `POST /v1/treasury/withdrawals` request is accepted, Tesser plans the route the funds will take and obtains a reference exchange rate. Tesser then sends a `withdrawal.quote_created` webhook — the first webhook fired for the withdrawal. The payload carries the planned `steps[]` array (each step with `status: "created"`) together with the populated `estimated` overlay at both the withdrawal and step levels. This webhook always fires, even for same-currency withdrawals, to keep the lifecycle uniform across resource types. For cross-currency withdrawals, the ratio of `estimated.from.amount` to `estimated.to.amount` is the indicative exchange rate at the off-ramp provider. Circle redemptions are 1:1, so for Scenario 2 the indicative rate matches the source amount. Example `withdrawal.quote_created` webhook (USDT from an OpenFX ledger off-ramped to your external USD bank account; swap step + transfer step): ```json { "id": "b5e9c3a7-2f4d-48e6-8b1c-7a3f9d2e5b4c", "type": "withdrawal.quote_created", "created_at": "2025-12-01T10:00:00.700Z", "data": { "object": { "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.700Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.700Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.700Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` :::note For Scenario 1, the swap step models OpenFX's USDT-to-USD redemption: USDT is debited and USD is credited at the same OpenFX ledger account (`estimated.from.account_id` and `estimated.to.account_id` both equal the OpenFX ledger UUID). The follow-on transfer step then pushes the redeemed USD from the OpenFX ledger to your external bank account. ::: Example `withdrawal.quote_created` webhook (USDC from a Circle ledger off-ramped via Circle redemption to your external USD bank account; swap step + transfer step): ```json { "id": "c7f3a9d2-5b8e-4f14-a1c6-3d9b2e7f4a5c", "type": "withdrawal.quote_created", "created_at": "2025-12-01T10:00:01.000Z", "data": { "object": { "id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "3a8e2f5c-9d4b-4f73-a2c0-6b1d3e9f7c4a", "transfer_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "6b9d4f2c-3e8a-4571-b0c6-8d2f1e9b4a3c", "transfer_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` :::note For Scenario 2, the swap step models Circle's USDC-to-USD redemption at parity (1:1, no slippage): USDC is debited and USD is credited at the same Circle ledger account. The follow-on transfer step then pushes the redeemed USD from the Circle ledger to your external bank account. ::: Example `withdrawal.quote_created` webhook (USDC on BASE from a self-custodial wallet, off-ramped via OpenFX to your external USD bank account; on-chain transfer + swap + bank transfer): ```json { "id": "d8e2c5a9-4b7f-4135-9c2d-6f3a1b8e4d7c", "type": "withdrawal.quote_created", "created_at": "2025-12-01T10:00:01.500Z", "data": { "object": { "id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "turnkey", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "6f4b9d3e-1a8c-4275-b0e6-5d2f8a3c9b1d", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 2, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "8e5c1f9a-3d7b-4148-a6c0-2f4d8b1e9c3a", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 3, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.500Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` :::note For Scenario 3, Step 1 is an on-chain transfer that delivers USDC from the BASE wallet to the OpenFX ledger account on BASE. Because the funds movement happens on chain, the step's `from.network` and `to.network` are both populated with `BASE` in the `estimated` and `actual` overlays. Step 2 is a swap at the OpenFX ledger that converts USDC into USD (`estimated.from.account_id` and `estimated.to.account_id` both equal the OpenFX ledger UUID); both `network` fields are `null` because the swap is purely a ledger-level adjustment. Step 3 pushes the redeemed USD from the OpenFX ledger to your external bank account. ::: ## Balance Check (`withdrawal.balance_updated`) After `withdrawal.quote_created` fires, Tesser reserves funds at the `desired.from.account_id` and emits `withdrawal.balance_updated` with the outcome. A `reserved` outcome means execution can proceed; an `awaiting_funds` outcome means the `desired.from.account_id` is short and Tesser will retry until `expires_at`. When `desired.from.account_id` is a self-custodial wallet (Scenario 3), the balance check on the wallet is performed as part of the step-signing flow — Tesser emits `step.signature_requested` first, and the reservation outcome follows once the signed step is submitted. See [Pre-Execution Checks](/overviews/funds-movement-lifecycle-and-data-model#pre-execution-checks) for the full description. Example `withdrawal.balance_updated` webhook (Scenario 1, balance reserved at the source OpenFX ledger): ```json { "id": "a4b6c8d0-3e5f-4a1b-8c2d-4e6f8a0b1c2d", "type": "withdrawal.balance_updated", "created_at": "2025-12-01T10:00:01.000Z", "data": { "object": { "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.700Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.700Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ## Withdrawal Step Execution Once funds have been reserved, Tesser begins executing each step in `step_sequence` order. The shape of step execution differs by scenario. ### Scenario 1: Cross-Currency Off-Ramp at OpenFX (USDT → USD → Bank) For Scenario 1, Step 1 is a swap at the OpenFX ledger that redeems USDT into USD. Tesser submits the trade, observes its acceptance, and observes its fill in close succession, so `step.submitted`, `step.confirmed`, and `step.completed` arrive close together. The swap's `estimated.from.account_id` and `estimated.to.account_id` are equal — both are the OpenFX ledger UUID `2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b`. Step 2 is the long-pole bank transfer that pushes the redeemed USD from the OpenFX ledger to your external bank account. It follows the standard step lifecycle: `step.submitted` fires when OpenFX accepts the wire instruction; `step.confirmed` fires when OpenFX's local payment network confirms acceptance of the outbound wire; `step.completed` fires when funds settle at the external bank account. Example `step.completed` webhook for Scenario 1, swap step (step 1): ```json { "id": "a4b9c5e2-7d3f-4218-9a6c-5e1f8d3b2a7c", "type": "step.completed", "created_at": "2025-12-01T10:02:00.500Z", "data": { "object": { "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.425", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:02:00.500Z", "submitted_at": "2025-12-01T10:02:00.000Z", "confirmed_at": "2025-12-01T10:02:00.300Z", "completed_at": "2025-12-01T10:02:00.500Z", "failed_at": null } } } ``` Example `step.completed` webhook for Scenario 1, off-ramp transfer step (step 2; terminal step): ```json { "id": "e1d6f4a3-9c8b-4f25-a7c0-3b1d8e9f4c5a", "type": "step.completed", "created_at": "2025-12-01T11:30:00.500Z", "data": { "object": { "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.425", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.425", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:30:00.500Z", "submitted_at": "2025-12-01T10:02:30.000Z", "confirmed_at": "2025-12-01T11:25:00.000Z", "completed_at": "2025-12-01T11:30:00.500Z", "failed_at": null } } } ``` ### Scenario 2: Cross-Currency Off-Ramp at Circle (USDC → USD → Bank) For Scenario 2, Circle's API exposes only terminal-state webhooks for both the redemption swap and the bank transfer. Tesser collapses each step's lifecycle directly to `step.completed` (or `step.failed`) — no intermediate `step.submitted` or `step.confirmed` events are emitted. See the note on collapsed step lifecycles in [Execution](/overviews/funds-movement-lifecycle-and-data-model#execution). - Step 1 is a `swap` step that redeems USDC into USD on Circle's books at the Circle ledger account. `estimated.from.currency` is `USDC` and `estimated.to.currency` is `USD`; both legs reference the Circle ledger account. Circle's redemption is 1:1. - Step 2 is a `transfer` step that pushes the redeemed USD from the Circle ledger account to your external USD bank account. Example `step.completed` webhook for Scenario 2, swap step (step 1): ```json { "id": "b6c2e8f4-3d9a-4517-a0c6-1e3f8b2d7a9c", "type": "step.completed", "created_at": "2025-12-01T10:02:00.500Z", "data": { "object": { "id": "3a8e2f5c-9d4b-4f73-a2c0-6b1d3e9f7c4a", "transfer_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:02:00.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": "2025-12-01T10:02:00.500Z", "failed_at": null } } } ``` Example `step.completed` webhook for Scenario 2, off-ramp transfer step (step 2; terminal step): ```json { "id": "c5f2a9d7-4b8e-4137-a6c0-3d1f8b9e7c5a", "type": "step.completed", "created_at": "2025-12-01T11:30:00.500Z", "data": { "object": { "id": "6b9d4f2c-3e8a-4571-b0c6-8d2f1e9b4a3c", "transfer_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:30:00.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": "2025-12-01T11:30:00.500Z", "failed_at": null } } } ``` ### Scenario 3: On-Chain Transfer From a Self-Custodial Wallet, Then Off-Ramp at OpenFX When the `desired.from.account_id` is a self-custodial wallet, Tesser cannot submit the on-chain transaction on its own — you must locally sign the unsigned transaction with your wallet's signing key. Tesser emits `step.signature_requested` once Step 1 is prepared, with the unsigned transaction available on the step DTO as `unsigned_transaction` (also retrievable via `GET /v1/treasury/withdrawals/{withdrawalId}`). You sign the unsigned transaction client-side, then submit the signature to `POST /v1/treasury/withdrawals/{withdrawalId}/steps/{stepId}/sign`. Tesser validates the signed transaction targets the prepared step and broadcasts on-chain — producing `step.signed`, `step.submitted` (with `transaction_hash`), `step.confirmed`, and finally `step.completed` for Step 1. Once Step 1 settles, Step 2 (swap at the OpenFX ledger, USDC → USD) executes synchronously: `step.submitted`, `step.confirmed`, and `step.completed` arrive in close succession. Step 3 (USD push from the OpenFX ledger to your external bank account) is the long pole and follows the standard step lifecycle. Example `step.signature_requested` webhook (Scenario 3, on-chain transfer from BASE wallet to OpenFX ledger): ```json { "id": "e4f6a8b0-1c3d-4e5f-9a7b-2c4d6e8f0a1b", "type": "step.signature_requested", "created_at": "2025-12-01T10:00:30.000Z", "data": { "object": { "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "unsigned_transaction": "0x02ed81893a850165a0bc0085012a05f200825208942e8f4c6b3a1d48e79b5c0f4d2c9a7e3b880de0b6b3a764000080c0", "provider_key": "turnkey", "status": "signature_requested", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:30.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } } } ``` 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 `POST /v1/treasury/withdrawals/{withdrawalId}/steps/{stepId}/sign` to execute the withdrawal step. Wallet and recipient addresses are automatically resolved from the step's account IDs; you only need to pass the step object. ```typescript import { LocalSigner, TesserApi } from "@tesser-payments/sdk"; // Initialize API client const 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 event const step = webhookPayload.data.object; // 2. Sign the step const result = await signer.signStep(step); // 3. Submit the signature to execute the step await fetch( `https://api.tesser.xyz/v1/treasury/withdrawals/${step.transfer_id}/steps/${step.id}/sign`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ signature: result.signature }), } ); ``` The `signStep` method returns: - `signature`: send this to the withdrawal sign endpoint to execute the step. - `unsignedTransaction`: the serialized unsigned transaction. - `metadata`: full signature details (`stampHeaderName`, `stampHeaderValue`, `body`) Tesser then broadcasts the signed transaction. After broadcast, you will receive `step.signed`, `step.submitted` (with `transaction_hash` populated), `step.confirmed`, and `step.completed` for Step 1. Example `step.completed` webhook for Scenario 3, on-chain transfer step (step 1): ```json { "id": "a5b7c9d1-3e5f-4a7b-8c9d-0e1f2a3b4c5d", "type": "step.completed", "created_at": "2025-12-01T10:05:35.000Z", "data": { "object": { "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": "BASE" } }, "fees": [], "transaction_hash": "0xb1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2", "provider_key": "turnkey", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:05:35.000Z", "submitted_at": "2025-12-01T10:04:05.000Z", "confirmed_at": "2025-12-01T10:04:07.500Z", "completed_at": "2025-12-01T10:05:35.000Z", "failed_at": null } } } ``` Example `step.completed` webhook for Scenario 3, swap step (step 2; USDC → USD at OpenFX): ```json { "id": "f7c3a8d4-2b9e-4516-a0c6-3d1f8b2e7c5a", "type": "step.completed", "created_at": "2025-12-01T10:06:00.500Z", "data": { "object": { "id": "6f4b9d3e-1a8c-4275-b0e6-5d2f8a3c9b1d", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 2, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.425", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:06:00.500Z", "submitted_at": "2025-12-01T10:06:00.000Z", "confirmed_at": "2025-12-01T10:06:00.300Z", "completed_at": "2025-12-01T10:06:00.500Z", "failed_at": null } } } ``` Example `step.completed` webhook for Scenario 3, off-ramp transfer step (step 3; terminal step): ```json { "id": "d2e8f5a3-4b9c-4617-a0c6-1f3d8b2e7c4a", "type": "step.completed", "created_at": "2025-12-01T11:30:00.500Z", "data": { "object": { "id": "8e5c1f9a-3d7b-4148-a6c0-2f4d8b1e9c3a", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 3, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.425", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.425", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:30:00.500Z", "submitted_at": "2025-12-01T10:06:30.000Z", "confirmed_at": "2025-12-01T11:25:00.000Z", "completed_at": "2025-12-01T11:30:00.500Z", "failed_at": null } } } ``` ## Withdrawal Info After Completion When the last step reaches a terminal state, Tesser populates the top-level `actual.*` overlay and emits a `withdrawal.updated` webhook carrying the full updated Withdrawal object. You can also retrieve the withdrawal at any time via [`GET /v1/treasury/withdrawals/{withdrawalId}`](/api/treasury#get-withdrawal-by-id) — the response payload below matches the body of the terminal `withdrawal.updated` webhook for each scenario. `desired.to.amount` remains `null` even after the withdrawal completes; the indicative target is in `estimated.to.amount` and the realized amount is in `actual.to.amount`. Example `GET /v1/treasury/withdrawals/{withdrawalId}` response (Scenario 1, complete): ```json { "data": { "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.425", "currency": "USD", "network": null } }, "steps": [ { "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.425", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:02:00.500Z", "submitted_at": "2025-12-01T10:02:00.000Z", "confirmed_at": "2025-12-01T10:02:00.300Z", "completed_at": "2025-12-01T10:02:00.500Z", "failed_at": null }, { "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.425", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.425", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:30:00.500Z", "submitted_at": "2025-12-01T10:02:30.000Z", "confirmed_at": "2025-12-01T11:25:00.000Z", "completed_at": "2025-12-01T11:30:00.500Z", "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:30:00.700Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example `GET /v1/treasury/withdrawals/{withdrawalId}` response (Scenario 2, complete): ```json { "data": { "id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "steps": [ { "id": "3a8e2f5c-9d4b-4f73-a2c0-6b1d3e9f7c4a", "transfer_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:02:00.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": "2025-12-01T10:02:00.500Z", "failed_at": null }, { "id": "6b9d4f2c-3e8a-4571-b0c6-8d2f1e9b4a3c", "transfer_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:30:00.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": "2025-12-01T11:30:00.500Z", "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:30:00.700Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example `GET /v1/treasury/withdrawals/{withdrawalId}` response (Scenario 3, complete): ```json { "data": { "id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:04:05.000Z", "desired": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.425", "currency": "USD", "network": null } }, "steps": [ { "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": "BASE" } }, "fees": [], "transaction_hash": "0xb1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2", "provider_key": "turnkey", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:05:35.000Z", "submitted_at": "2025-12-01T10:04:05.000Z", "confirmed_at": "2025-12-01T10:04:07.500Z", "completed_at": "2025-12-01T10:05:35.000Z", "failed_at": null }, { "id": "6f4b9d3e-1a8c-4275-b0e6-5d2f8a3c9b1d", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 2, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.425", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:06:00.500Z", "submitted_at": "2025-12-01T10:06:00.000Z", "confirmed_at": "2025-12-01T10:06:00.300Z", "completed_at": "2025-12-01T10:06:00.500Z", "failed_at": null }, { "id": "8e5c1f9a-3d7b-4148-a6c0-2f4d8b1e9c3a", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 3, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.425", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.425", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:30:00.500Z", "submitted_at": "2025-12-01T10:06:30.000Z", "confirmed_at": "2025-12-01T11:25:00.000Z", "completed_at": "2025-12-01T11:30:00.500Z", "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:30:00.700Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` ## Webhook Events by Scenario Below are the webhook events you will observe for each of the three withdrawal scenarios in this guide. **Scenario 1 — Cross-currency off-ramp at OpenFX (USDT → USD → bank)** | # | What happens | What you receive / do | Fields populated | |---|---|---|---| | 1 | You submit the withdrawal request | HTTP response with `id`; `balance_status: "unreserved"` | `desired.from.amount` | | 2 | Tesser plans the route and obtains a quote | `withdrawal.quote_created` webhook fires with 2 steps and the populated `estimated` overlay | Steps array; `estimated.from.amount` and `estimated.to.amount` at withdrawal and step level (reflects indicative rate) | | 3 | Tesser reserves USDT at the `desired.from.account_id` | `withdrawal.balance_updated` webhook with `balance_status: "reserved"` | `balance_status`, `balance_reserved_at` | | 4 | OpenFX redeems USDT for USD on its books | `step.submitted`, `step.confirmed`, and `step.completed` fire on the swap step in close succession | Step 1: `actual.from.amount`, `actual.to.amount` (reflects actual fill rate), all timestamps | | 5 | OpenFX initiates the wire to the external bank | `step.submitted` on the transfer step | Step 2: `submitted_at`, `actual.from.amount` | | 6 | OpenFX confirms acceptance | `step.confirmed` on the transfer step | Step 2: `confirmed_at` | | 7 | Funds settle at the external bank; withdrawal is complete | `step.completed` on the transfer step, followed by `withdrawal.updated` with the terminal Withdrawal object | Step 2: `completed_at`, `actual.to.amount`. Withdrawal-level `actual.from.amount` and `actual.to.amount`. | **Scenario 2 — Cross-currency off-ramp at Circle (USDC → USD → bank)** | # | What happens | What you receive / do | Fields populated | |---|---|---|---| | 1 | You submit the withdrawal request | HTTP response with `id`; `balance_status: "unreserved"` | `desired.from.amount` | | 2 | Tesser plans the route and obtains a quote | `withdrawal.quote_created` webhook fires with 2 steps and the populated `estimated` overlay | Steps array; `estimated.from.amount` and `estimated.to.amount` at withdrawal and step level (1:1) | | 3 | Tesser reserves USDC at the `desired.from.account_id` | `withdrawal.balance_updated` webhook with `balance_status: "reserved"` | `balance_status`, `balance_reserved_at` | | 4 | Circle redeems USDC for USD on its books | A terminal `step.completed` event fires for the swap step (no intermediate `submitted`/`confirmed` events from Circle) | Step 1: `actual.from.amount`, `actual.to.amount`, `completed_at` | | 5 | Circle pushes USD to the external bank account | A terminal `step.completed` event fires for the transfer step, followed by `withdrawal.updated` with the terminal Withdrawal object | Step 2: `actual.from.amount`, `actual.to.amount`, `completed_at`. Withdrawal-level `actual.from.amount` and `actual.to.amount`. | **Scenario 3 — On-chain transfer from a self-custodial wallet, then off-ramp at OpenFX** | # | What happens | What you receive / do | Fields populated | |---|---|---|---| | 1 | You submit the withdrawal request | HTTP response with `id`; `balance_status: "unreserved"` | `desired.from.amount` | | 2 | Tesser plans the route and obtains a quote | `withdrawal.quote_created` webhook fires with 3 steps and the populated `estimated` overlay | Steps array; `estimated.from.amount` and `estimated.to.amount` at withdrawal and step level | | 3 | Tesser asks you to sign the on-chain transfer | `step.signature_requested` on Step 1 | Step 1: `status` updates; signing payload supplied via the API | | 4 | You submit the signature | `POST /v1/treasury/withdrawals/{withdrawalId}/steps/{stepId}/sign` with `{signature}`; `step.signed` fires; `withdrawal.balance_updated` with `balance_status: "reserved"` once the signed step is accepted | `balance_status`, `balance_reserved_at`; Step 1 `status: "signed"` | | 5 | Tesser broadcasts the signed transaction | `step.submitted` on Step 1 | Step 1: `submitted_at`, `transaction_hash` | | 6 | The on-chain transaction is visible on the network | `step.confirmed` on Step 1 | Step 1: `confirmed_at` | | 7 | The transfer reaches finality at the OpenFX ledger | `step.completed` on Step 1 | Step 1: `completed_at`, `actual.from.amount`, `actual.to.amount` | | 8 | OpenFX redeems USDC for USD on its books | `step.submitted`, `step.confirmed`, and `step.completed` fire on the swap step in close succession | Step 2: `actual.from.amount`, `actual.to.amount` (reflects actual fill rate), all timestamps | | 9 | OpenFX initiates the wire to the external bank | `step.submitted` on the transfer step | Step 3: `submitted_at`, `actual.from.amount` | | 10 | OpenFX confirms acceptance | `step.confirmed` on the transfer step | Step 3: `confirmed_at` | | 11 | Funds settle at the external bank; withdrawal is complete | `step.completed` on the transfer step, followed by `withdrawal.updated` with the terminal Withdrawal object | Step 3: `completed_at`, `actual.to.amount`. Withdrawal-level `actual.from.amount` and `actual.to.amount`. | ## Failure Modes for Withdrawals If a withdrawal fails, the top-level `actual.*` fields populate only when at least one step has reached `step.status = completed`. With one or more completed steps, the top-level `actual.from` matches the first completed step's `actual.from`, and the top-level `actual.to` matches the last completed step's `actual.to`. With no completed step, the top-level `actual.*` stays all-null — `desired.*` and `estimated.*` describe the originally requested and quoted state, while step-level `status_reasons` carries the cause of the failure. Failed steps always have all-null `actual.*`. Any steps subsequent to the failure step also transition to `failed` with null `actual.*` fields. ### Off-Ramp Trade Fails to Complete (Scenarios 1, 2, or 3) If the liquidity provider's swap step cannot complete before the withdrawal's `expires_at` timestamp, the swap step transitions to `failed` and the subsequent transfer step also fails because there were no off-ramped funds to push to the external bank. In Scenarios 1 and 2 (where the swap is the only execution work), no step reaches `step.status = completed` — top-level `actual.*` stays all-null per the Failure Modes rule. In Scenario 3, the prior on-chain transfer step (Step 1) had already completed, so top-level `actual.from` and `actual.to` populate from Step 1's `actual.*` (USDC at the OpenFX ledger). What you will observe (illustration uses Scenario 1 — OpenFX swap give-up): - `step.failed` on the swap step. Step-level `actual.*` is null; `status_reasons` carries the failure detail. - `step.failed` on the off-ramp transfer step. This step never entered `submitted`, because there were no funds to push to the external bank. - The withdrawal's top-level `actual.*` is all null because no step reached `completed`. `desired.to.account_id` stays as the originally requested external bank account (client intent is never overwritten); `desired.from` and `estimated.*` describe what was requested and planned. - A `withdrawal.updated` webhook fires alongside the terminal `step.failed` events, carrying the full updated Withdrawal object. - The USDT balance at the OpenFX ledger remains in your account and is available to use for a future rebalance or withdrawal. Example `step.failed` webhook on Step 1 (swap, never filled): ```json { "id": "f2c8e4a3-9b7d-4f15-a6c0-2d3f8e1b9c4a", "type": "step.failed", "created_at": "2025-12-01T14:00:00.000Z", "data": { "object": { "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": "2025-12-01T10:32:18.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `step.failed` webhook on Step 2 (off-ramp transfer, never submitted): ```json { "id": "a9d3e7c4-2f8b-4516-a0c6-3e1f9b2a8c5d", "type": "step.failed", "created_at": "2025-12-01T14:00:00.100Z", "data": { "object": { "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.100Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.100Z" } } } ``` Example `withdrawal.updated` webhook (Scenario 1, swap give-up; USDT remains at OpenFX ledger): ```json { "id": "b3c9f4e7-2d8a-4561-9b0c-4d3f1e9b2a7c", "type": "withdrawal.updated", "created_at": "2025-12-01T14:00:00.300Z", "data": { "object": { "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": "2025-12-01T10:32:18.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" }, { "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.100Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.100Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.300Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ### External Bank Transfer Fails (Any Scenario) If the off-ramp swap succeeded but the liquidity provider's wire to the external bank cannot be confirmed before the withdrawal's `expires_at` timestamp, the transfer step transitions to `failed`. The top-level `actual.to` resolves to USD held at the liquidity provider's ledger — the redeemed USD never reached the external bank. The illustration below uses Scenario 2 (post-Circle redemption) — Step 1 (swap) completed successfully, and Step 2 (bank transfer) then failed. What you will observe: - `step.completed` on the swap step (step 1) earlier in the lifecycle — funds redeemed from USDC into USD at the Circle ledger. - `step.failed` on the bank transfer step (step 2) when `expires_at` is reached without confirmation. Step-level `actual.*` is null; `status_reasons` carries the failure detail. - The withdrawal's top-level `actual.to.account_id` resolves to the Circle ledger UUID `44031e7e-d416-45f0-a46b-ded12b9751ca`, with `actual.to.currency: "USD"` and `actual.to.amount` equal to the redeemed amount. `desired.to.account_id` stays as the originally requested external bank account. - A `withdrawal.updated` webhook fires alongside the terminal `step.failed`, carrying the full updated Withdrawal object with the populated `actual.*` overlay. - The USD balance at the Circle ledger is available to use for a future rebalance or withdrawal. Example `step.failed` webhook for step 2 (bank transfer; Scenario 2 post-redemption): ```json { "id": "d4e8f2a7-3c9b-4615-a6c0-1d2f3e9b4c7a", "type": "step.failed", "created_at": "2025-12-01T14:00:00.000Z", "data": { "object": { "id": "6b9d4f2c-3e8a-4571-b0c6-8d2f1e9b4a3c", "transfer_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": "2025-12-01T10:02:30.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `withdrawal.updated` webhook (Scenario 2, bank transfer failed post-redemption; USD remains at Circle ledger): ```json { "id": "e5f3a7d2-4b8c-4561-9a0d-2c3f1e9b8a4c", "type": "withdrawal.updated", "created_at": "2025-12-01T14:00:00.300Z", "data": { "object": { "id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null } }, "steps": [ { "id": "3a8e2f5c-9d4b-4f73-a2c0-6b1d3e9f7c4a", "transfer_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:02:00.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": "2025-12-01T10:02:00.500Z", "failed_at": null }, { "id": "6b9d4f2c-3e8a-4571-b0c6-8d2f1e9b4a3c", "transfer_id": "9b4d7c2e-5f8a-43b1-ae6d-2f7c9e3b1a8d", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": "2025-12-01T10:02:30.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.300Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ### On-Chain Transfer From Wallet Fails (Scenario 3) If Step 1's on-chain transfer from the self-custodial wallet to the OpenFX ledger fails — for example, because of a gas issue, nonce conflict, chain reorg, or the signed step never being submitted — the withdrawal terminates with USDC remaining at the source wallet. The subsequent swap and transfer steps never start. What you will observe: - `step.failed` on Step 1 (on-chain transfer). Step-level `actual.*` is null; `status_reasons` carries the failure detail. - `step.failed` on Step 2 (swap) and Step 3 (bank transfer). Neither step was ever submitted. - The withdrawal's top-level `actual.*` is all null because no step reached `completed`. `desired.to.account_id` stays as the originally requested external bank account; `desired.from` and `estimated.*` describe what was requested and planned. - A `withdrawal.updated` webhook fires alongside the terminal `step.failed` events, carrying the full updated Withdrawal object. Example `step.failed` webhook for Step 1 (on-chain transfer from BASE wallet, never confirmed): ```json { "id": "fb4c5d6e-7f8a-4b9c-9d0e-1f2a3b4c5d6e", "type": "step.failed", "created_at": "2025-12-01T14:00:00.000Z", "data": { "object": { "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "turnkey", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `withdrawal.updated` webhook (Scenario 3, on-chain transfer failed; USDC remains at the source wallet): ```json { "id": "ec7d3a9f-2b4c-4561-a0d8-3e1f5b9c4a7d", "type": "withdrawal.updated", "created_at": "2025-12-01T14:00:00.300Z", "data": { "object": { "id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "2c8a4e7b-5f1d-4936-a8c0-3e9b1d6f2a5c", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "turnkey", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" }, { "id": "6f4b9d3e-1a8c-4275-b0e6-5d2f8a3c9b1d", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 2, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.100Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.100Z" }, { "id": "8e5c1f9a-3d7b-4148-a6c0-2f4d8b1e9c3a", "transfer_id": "5d7c3a9e-2b4f-4861-9c0d-8e3f1b2d4a7c", "step_sequence": 3, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.100Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.100Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.300Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ### Insufficient Funds If the `desired.from.account_id` does not have enough balance to cover `desired.from.amount`, the balance check returns `awaiting_funds` instead of `reserved`. For provider-ledger sources (Scenarios 1 and 2), Tesser fires `withdrawal.balance_updated` with `balance_status: "awaiting_funds"` shortly after creation and queues the withdrawal, retrying the reservation as the balance at `desired.from.account_id` changes (e.g., as a deposit lands or another withdrawal frees funds). For self-custodial wallet sources (Scenario 3), the balance check happens during step signing — when you submit the signature, Tesser detects the wallet is short and fires `withdrawal.balance_updated` with `balance_status: "awaiting_funds"`; the same timeout pattern applies. If the reservation does not succeed before `expires_at` in any scenario, the withdrawal times out: every step transitions to `failed`, `step.failed` events fire for each, and a final `withdrawal.updated` webhook reports the terminal state with `actual.*` null because no funds moved. The example below illustrates Scenario 1; the same pattern applies in Scenarios 2 and 3, with one `failed` step entry per never-started step. What you will observe: - `withdrawal.balance_updated` with `balance_status: "awaiting_funds"` shortly after creation (or after step signing in Scenario 3). - (Optional) further `withdrawal.balance_updated` events as balance changes are detected. - At `expires_at`: `step.failed` for every step, followed by `withdrawal.updated` with the terminal Withdrawal object. Example `withdrawal.balance_updated` webhook (Scenario 1, source ledger short): ```json { "id": "ac1b3d5e-7f9a-4b2c-8d4e-6f8a0b2c4d6e", "type": "withdrawal.balance_updated", "created_at": "2025-12-01T10:00:01.000Z", "data": { "object": { "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "awaiting_funds", "balance_reserved_at": null, "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.700Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.700Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `withdrawal.updated` webhook (Scenario 1, source ledger remained underfunded; withdrawal expired): ```json { "id": "ed6f8a0b-2c4e-4f6a-9b0c-3d5f7a9b1c3e", "type": "withdrawal.updated", "created_at": "2025-12-01T14:00:00.500Z", "data": { "object": { "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "outbound", "balance_status": "awaiting_funds", "balance_reserved_at": null, "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8d3f1c6a-4b9e-4275-9a8d-2c5e7f4b1a3d", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USDT", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" }, { "id": "9e4d2a7c-1f5b-4836-a0c2-7d6e3f1b9c8a", "transfer_id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USD", "network": null }, "to": { "account_id": "c4a7e9b3-5d2f-4e8a-b6c1-9f3a7d2e8b4f", "amount": "999.475", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "withdrawals-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.100Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.100Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.500Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` --- ## Document: Rebalance Funds Move funds between your managed accounts, including across currencies and networks. URL: /how-tos/rebalance-funds # Rebalance Funds Rebalances move funds between managed accounts on the Tesser platform. Rebalances can be same-currency as well as cross-currency (that include a swap step). Common patterns include moving funds between two ledgers at the same provider, between a provider ledger and a self-custodial wallet, or between two self-custodial wallets. Before creating a rebalance, review the [Funds Movement Lifecycle and Data Model](/overviews/funds-movement-lifecycle-and-data-model) overview to understand the shared data shape, lifecycle phases, and statuses that apply to rebalances and Tesser's other funds-movement resources. ## Prerequisites The `desired.from.account_id` and `desired.to.account_id` must each be one of your managed accounts — either a provider ledger (e.g., a Circle ledger or an OpenFX ledger) or a self-custodial wallet you have registered with Tesser. For more information on registering accounts, see [Create an account](/how-tos/create-an-account). Rebalances are first-party only — they move funds between your own managed accounts and never involve a third party. If you want to send funds to a third party, see [Send a Stablecoin Payout](/how-tos/send-a-stablecoin-payout/create-a-stablecoin-payout) and [Create a counterparty](/how-tos/create-a-counterparty). For Scenario 2 in this guide, the source ledger is at OpenFX and assumes USD has already landed at that ledger via a fiat deposit. See [Deposit Funds via a Liquidity Provider](/how-tos/deposit-funds-via-a-liquidity-provider) for the earlier part of the deposit lifecycle. ## Rebalance Workflow A rebalance executes as one or more steps. Tesser plans the route synchronously when you submit the create request, then drives each step through the shared step lifecycle. - A `transfer` step moves funds from one account to another. Same-currency moves between two ledgers, between a ledger and a wallet, or between two wallets are all `transfer` steps. - A `swap` step exchanges currencies inside a single account. The step's `estimated.from.account_id` and `estimated.to.account_id` are the same. Cross-currency rebalances start with a swap step at a provider ledger, then transfer the swapped balance. For a complete description of how rebalances move through planning, balance reservation, execution, and terminal state, see the lifecycle overview's [Planning](/overviews/funds-movement-lifecycle-and-data-model#planning), [Execution](/overviews/funds-movement-lifecycle-and-data-model#execution), and [Terminal State and Divergence](/overviews/funds-movement-lifecycle-and-data-model#terminal-state-and-divergence) sections. Step statuses are documented under [Step Statuses](/overviews/funds-movement-lifecycle-and-data-model#step-statuses). ## Exchange Rates for Cross-Token Rebalances When a rebalance crosses currencies, Tesser sources an indicative quote from the relevant liquidity provider and reports it via the `estimated.from.amount` and `estimated.to.amount` fields. The actual fill rate may differ slightly from the indicative quote and is reflected in `actual.*` once the swap step completes. See [Planning](/overviews/funds-movement-lifecycle-and-data-model#planning) for how Tesser obtains and reports quotes. | Type of liquidity provider | Example | Behavior | |---|---|---| | 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 | For same-currency rebalances, `estimated.*` matches `desired.*` (1:1) and there is no swap step. ## Rebalance Creation Submit a request to [POST /v1/treasury/rebalances](/api/treasury#create-rebalance). - If applicable, you should specify on which `tenant`'s behalf you are requesting the rebalance. - For the rebalance, populate the following fields in the `desired` object: - `desired.from.account_id`: The identifier of the account funds will be moved from. - `desired.from.amount`: The amount of `desired.from.currency` to move from the source. - `desired.from.currency`: The currency to move from the source. - `desired.from.network`: The network of the funds at `desired.from.account_id` (only applicable when `desired.from.account_id` is a wallet). - `desired.to.account_id`: The identifier of the destination account. - `desired.to.currency`: The currency to land at the destination. - `desired.to.network`: The destination network (only applicable when the destination account is a wallet). *Note: `desired.to.amount` is not auto-populated. The Rebalance'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:** 1. Same-token rebalance between two ledger accounts at Circle (USDC, no network) 2. Cross-token rebalance from a USD ledger at OpenFX to a self-custodial wallet on BASE (USDC) 3. Same-token rebalance between two self-custodial wallets (USDT on POLYGON) Example request (rebalance USDC between two Circle ledger accounts): ```json { "tenant_id": null, "desired": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC" }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "currency": "USDC" } } } ``` Example request (rebalance USD at an OpenFX ledger into USDC at a self-custodial wallet on BASE): ```json { "tenant_id": null, "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "currency": "USDC", "network": "BASE" } } } ``` Example request (rebalance USDT on POLYGON between two self-custodial wallets): ```json { "tenant_id": null, "desired": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "currency": "USDT", "network": "POLYGON" } } } ``` In the API response, Tesser will create and return an `id` for the rebalance request. At creation, `balance_status` is `unreserved` and `balance_reserved_at` is `null` — both update once the balance check completes. Example response (rebalance USDC between two Circle ledger accounts): ```json { "data": { "id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example response (rebalance USD at an OpenFX ledger into USDC at a self-custodial wallet on BASE): ```json { "data": { "id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": null, "currency": "USDC", "network": "BASE" } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example response (rebalance USDT on POLYGON between two self-custodial wallets): ```json { "data": { "id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": null, "currency": "USDT", "network": "POLYGON" } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` ## Rebalance Quote Created (`rebalance.quote_created`) After your `POST /v1/treasury/rebalances` request is accepted, Tesser plans the route the funds will take and obtains a reference exchange rate. Tesser then sends a `rebalance.quote_created` webhook — the first webhook fired for the rebalance. The payload carries the planned `steps[]` array (each step with `status: "created"`) together with the populated `estimated` overlay at both the rebalance and step levels. This webhook always fires, even for same-token same-network rebalances, to keep the lifecycle uniform across resource types and to future-proof bridging across networks. For same-currency rebalances, the ratio of `estimated.from.amount` to `estimated.to.amount` is 1:1. For cross-currency rebalances, the ratio is the indicative exchange rate at the liquidity provider. Example `rebalance.quote_created` webhook (rebalance USDC between two Circle ledger accounts): ```json { "id": "b8c4d5e6-2f3a-4b7c-9d8e-1f2a3b4c5d6e", "type": "rebalance.quote_created", "created_at": "2025-12-01T10:00:00.600Z", "data": { "object": { "id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `rebalance.quote_created` webhook (rebalance USD at an OpenFX ledger into USDC at a self-custodial wallet on BASE): ```json { "id": "c2d4e6f8-1a3b-4c5d-9e7f-2a4b6c8d0e1f", "type": "rebalance.quote_created", "created_at": "2025-12-01T10:00:00.600Z", "data": { "object": { "id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": null, "currency": "USDC", "network": "BASE" } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "4d6f7a8b-1c2d-4e5f-8a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "5e7f8a9b-2c3d-4e5f-9a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `rebalance.quote_created` webhook (rebalance USDT on POLYGON between two self-custodial wallets): ```json { "id": "d3e5f7a9-2b4c-4d6e-8f0a-3b5c7d9e1f2a", "type": "rebalance.quote_created", "created_at": "2025-12-01T10:00:00.600Z", "data": { "object": { "id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "unreserved", "balance_reserved_at": null, "desired": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": null, "currency": "USDT", "network": "POLYGON" } }, "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": "1000", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": "1000", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "turnkey", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ## Balance Check (`rebalance.balance_updated`) After `rebalance.quote_created` fires, Tesser attempts to reserve funds from the `desired.from.account_id` and emits `rebalance.balance_updated` with the outcome. A "reserved" `balance_status` means execution can proceed; an "awaiting_funds" status means the `desired.from.account_id` does not have sufficient funds. If the `desired.from.account_id` does not have sufficient funds at creation time, the rebalance remains in `awaiting_funds` until funds arrive or the rebalance expires. If sufficient funds are added to the `desired.from.account_id` prior to expiration, Tesser will update the `balance_status` and republish the `rebalance.balance_updated` webhook. The terminal `balance_reserved_at` timestamp records when the reservation succeeded. See [Balance Statuses](/overviews/funds-movement-lifecycle-and-data-model#balance-statuses). When `desired.from.account_id` is a self-custodial wallet (Scenario 3), the balance check is performed as part of the step-signing flow — Tesser emits `step.signature_requested` first, and the reservation outcome follows once the signed step is submitted. See [Pre-Execution Checks](/overviews/funds-movement-lifecycle-and-data-model#pre-execution-checks) for the full description. Example `rebalance.balance_updated` webhook (Scenario 1, balance reserved at the source Circle ledger): ```json { "id": "a4b6c8d0-3e5f-4a1b-8c2d-4e6f8a0b1c2d", "type": "rebalance.balance_updated", "created_at": "2025-12-01T10:00:01.000Z", "data": { "object": { "id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ## Rebalance Step Execution Once funds have been reserved, Tesser begins executing each step in `step_sequence` order. The events you observe depend on whether the step has on-chain visibility and whether it requires a wallet signature. ### Scenario 1: Circle-Internal Transfer Internal moves between two Circle ledger accounts surface only a terminal webhook from the provider, so Tesser emits a single `step.completed` event for the step. There are no intermediate `step.submitted` or `step.confirmed` events. The terminal `step.completed` is followed by a `rebalance.updated` event carrying the full updated Rebalance object. Example `step.completed` webhook (Scenario 1, single Circle internal transfer): ```json { "id": "f1e2d3c4-5b6a-4798-8e0d-3f4a5b6c7d8e", "type": "step.completed", "created_at": "2025-12-01T10:05:00.500Z", "data": { "object": { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:05:00.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": "2025-12-01T10:05:00.500Z", "failed_at": null } } } ``` :::note For Circle-internal moves, `transaction_hash` is always `null`. ::: ### Scenario 2: Swap at OpenFX, Then On-Chain Transfer to a Wallet Step 1 is a swap at the OpenFX ledger. Tesser submits the trade, observes its acceptance, and observes its fill in close succession, so `step.submitted`, `step.confirmed`, and `step.completed` arrive close together. The swap's `estimated.from.account_id` and `estimated.to.account_id` are equal — both are the OpenFX ledger UUID `2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b`. Step 2 is an on-chain transfer that moves the swapped USDC to the BASE wallet. The transfer originates from the OpenFX ledger and follows the full step lifecycle — `step.submitted`, `step.confirmed`, and `step.completed` — with the `transaction_hash` populated once Tesser observes the on-chain transaction. Example `step.completed` webhook (Scenario 2, on-chain transfer to a BASE wallet): ```json { "id": "c2e5f3d1-7b9a-4c48-8e0d-3f4a5b6c7d8e", "type": "step.completed", "created_at": "2025-12-01T10:36:35.000Z", "data": { "object": { "id": "5e7f8a9b-2c3d-4e5f-9a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.475", "currency": "USDC", "network": "BASE" } }, "fees": [], "transaction_hash": "0x7e8c3f2a1b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f", "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:36:35.000Z", "submitted_at": "2025-12-01T10:35:05.000Z", "confirmed_at": "2025-12-01T10:35:07.500Z", "completed_at": "2025-12-01T10:36:35.000Z", "failed_at": null } } } ``` ### Scenario 3: On-Chain Transfer Between Self-Custodial Wallets When the `desired.from.account_id` is a self-custodial wallet, Tesser cannot submit the on-chain transaction on its own — you must locally sign the unsigned transaction with your wallet's signing key. Tesser emits `step.signature_requested` once the step is prepared, with the unsigned transaction available on the step DTO as `unsigned_transaction` (also retrievable via `GET /v1/treasury/rebalances/{rebalanceId}`). You sign client-side, then submit the resulting signature to `POST /v1/treasury/rebalances/{rebalanceId}/steps/{stepId}/sign`. Tesser validates the signed transaction targets the prepared step and broadcasts on-chain — producing `step.signed`, `step.submitted` (with `transaction_hash`), `step.confirmed`, and finally `step.completed`. Example `step.signature_requested` webhook (Scenario 3, on-chain wallet-to-wallet transfer): ```json { "id": "e4f6a8b0-1c3d-4e5f-9a7b-2c4d6e8f0a1b", "type": "step.signature_requested", "created_at": "2025-12-01T10:00:30.000Z", "data": { "object": { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": "1000", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "unsigned_transaction": "0x02ed81893a850165a0bc0085012a05f200825208949c4e7f2b1d8a4e6cb3f58a2d6e9b1c4f880de0b6b3a764000080c0", "provider_key": "turnkey", "status": "signature_requested", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:30.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } } } ``` 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 `POST /v1/treasury/rebalances/{rebalanceId}/steps/{stepId}/sign` to execute the rebalance step. Wallet and recipient addresses are automatically resolved from the step's account IDs; you only need to pass the step object. ```typescript import { LocalSigner, TesserApi } from "@tesser-payments/sdk"; // Initialize API client const 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 event const step = webhookPayload.data.object; // 2. Sign the step const result = await signer.signStep(step); // 3. Submit the signature to execute the step await fetch( `https://api.tesser.xyz/v1/treasury/rebalances/${step.transfer_id}/steps/${step.id}/sign`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ signature: result.signature }), } ); ``` The `signStep` method returns: - `signature`: send this to the rebalance step sign API to execute the step. - `unsignedTransaction`: the serialized unsigned transaction (matches `unsigned_transaction` on the step DTO) - `metadata`: full signature details (`stampHeaderName`, `stampHeaderValue`, `body`) Tesser then broadcasts the signed transaction. After broadcast, you will receive `step.signed`, `step.submitted` (with `transaction_hash` populated), `step.confirmed`, and `step.completed`. Example `step.completed` webhook (Scenario 3, on-chain wallet-to-wallet transfer): ```json { "id": "a5b7c9d1-3e5f-4a7b-8c9d-0e1f2a3b4c5d", "type": "step.completed", "created_at": "2025-12-01T10:05:35.000Z", "data": { "object": { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": "1000", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": "1000", "currency": "USDT", "network": "POLYGON" } }, "fees": [], "transaction_hash": "0xb1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2", "provider_key": "turnkey", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:05:35.000Z", "submitted_at": "2025-12-01T10:04:05.000Z", "confirmed_at": "2025-12-01T10:04:07.500Z", "completed_at": "2025-12-01T10:05:35.000Z", "failed_at": null } } } ``` ## Rebalance Info After Completion When the last step reaches a terminal state, Tesser populates the top-level `actual.*` overlay and emits a `rebalance.updated` webhook carrying the full updated Rebalance object. You can also retrieve the rebalance at any time via [`GET /v1/treasury/rebalances/{rebalanceId}`](/api/treasury#get-rebalance-by-id) — the response payload below matches the body of the terminal `rebalance.updated` webhook for each scenario. `desired.to.amount` remains `null` even after the rebalance completes; the indicative target is in `estimated.to.amount` and the realized amount is in `actual.to.amount`. Example `GET /v1/treasury/rebalances/{rebalanceId}` response (Scenario 1, complete): ```json { "data": { "id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:05:00.500Z", "submitted_at": null, "confirmed_at": null, "completed_at": "2025-12-01T10:05:00.500Z", "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:05:00.700Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example `GET /v1/treasury/rebalances/{rebalanceId}` response (Scenario 2, complete with 0.5 bps slippage on the USDC fill): ```json { "data": { "id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": null, "currency": "USDC", "network": "BASE" } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.475", "currency": "USDC", "network": "BASE" } }, "steps": [ { "id": "4d6f7a8b-1c2d-4e5f-8a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USDC", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:33:00.500Z", "submitted_at": "2025-12-01T10:33:00.100Z", "confirmed_at": "2025-12-01T10:33:00.300Z", "completed_at": "2025-12-01T10:33:00.500Z", "failed_at": null }, { "id": "5e7f8a9b-2c3d-4e5f-9a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.475", "currency": "USDC", "network": "BASE" } }, "fees": [], "transaction_hash": "0x7e8c3f2a1b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f", "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:36:35.000Z", "submitted_at": "2025-12-01T10:35:05.000Z", "confirmed_at": "2025-12-01T10:35:07.500Z", "completed_at": "2025-12-01T10:36:35.000Z", "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:36:35.200Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example `GET /v1/treasury/rebalances/{rebalanceId}` response (Scenario 3, complete): ```json { "data": { "id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": null, "currency": "USDT", "network": "POLYGON" } }, "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": "1000", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": "1000", "currency": "USDT", "network": "POLYGON" } }, "steps": [ { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": "1000", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "1000", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f", "amount": "1000", "currency": "USDT", "network": "POLYGON" } }, "fees": [], "transaction_hash": "0xb1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2", "provider_key": "turnkey", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:05:35.000Z", "submitted_at": "2025-12-01T10:04:05.000Z", "confirmed_at": "2025-12-01T10:04:07.500Z", "completed_at": "2025-12-01T10:05:35.000Z", "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:05:35.200Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` ## Webhook Events by Scenario Below are the webhook events you will observe for each of the three rebalance scenarios in this guide. **Scenario 1 — Same-token rebalance between two Circle ledger accounts** | # | What happens | What you receive / do | Fields populated | |---|---|---|---| | 1 | You submit the rebalance request | HTTP response with `id`; `balance_status: "unreserved"` | `desired.from.amount` | | 2 | Tesser plans the route and obtains a quote | `rebalance.quote_created` webhook fires with 1 step and the populated `estimated` overlay | Steps array; `estimated.from.amount` and `estimated.to.amount` at rebalance and step level (1:1) | | 3 | Tesser reserves funds at the `desired.from.account_id` | `rebalance.balance_updated` webhook with `balance_status: "reserved"` | `balance_status`, `balance_reserved_at` | | 4 | Circle moves the USDC between ledger accounts | A terminal `step.completed` event fires for the single step, followed by a `rebalance.updated` event carrying the full Rebalance with `actual.*` populated. Tesser observes Circle's single terminal webhook, so no intermediate `step.submitted` or `step.confirmed` events are emitted. | Step 1: `actual.from.amount`, `actual.to.amount`, `completed_at`. Rebalance-level `actual.from.amount` and `actual.to.amount`. Rebalance complete. | **Scenario 2 — Cross-token rebalance from OpenFX ledger to a BASE wallet** | # | What happens | What you receive / do | Fields populated | |---|---|---|---| | 1 | You submit the rebalance request | HTTP response with `id`; `balance_status: "unreserved"` | `desired.from.amount` | | 2 | Tesser plans the route and obtains a quote | `rebalance.quote_created` webhook fires with 2 steps and the populated `estimated` overlay | Steps array; `estimated.from.amount` and `estimated.to.amount` at rebalance and step level | | 3 | Tesser reserves USD at the `desired.from.account_id` | `rebalance.balance_updated` webhook with `balance_status: "reserved"` | `balance_status`, `balance_reserved_at` | | 4 | Tesser executes the swap (USD → USDC) at the OpenFX ledger | `step.submitted`, `step.confirmed`, and `step.completed` fire on the swap step in close succession | Step 1: `actual.from.amount`, `actual.to.amount` (reflects actual fill rate), all timestamps | | 5 | Tesser submits the on-chain transfer of USDC to the BASE wallet | `step.submitted` on the transfer step | Step 2: `submitted_at`, `actual.from.amount` | | 6 | The on-chain transaction is visible on the network | `step.confirmed` on the transfer step | Step 2: `confirmed_at`, `transaction_hash` | | 7 | The transfer reaches finality; rebalance is complete | `step.completed` on the transfer step, followed by `rebalance.updated` with the terminal Rebalance object | Step 2: `completed_at`, `actual.to.amount`. Rebalance-level `actual.to.amount`. | **Scenario 3 — Same-token rebalance between two self-custodial wallets** | # | What happens | What you receive / do | Fields populated | |---|---|---|---| | 1 | You submit the rebalance request | HTTP response with `id`; `balance_status: "unreserved"` | `desired.from.amount` | | 2 | Tesser plans the route and obtains a quote | `rebalance.quote_created` webhook fires with 1 step and the populated `estimated` overlay | Steps array; `estimated.from.amount` and `estimated.to.amount` at rebalance and step level (1:1) | | 3 | Tesser asks you to sign the on-chain transfer | `step.signature_requested` on the step | Step 1: `status` updates; signing payload supplied via the API | | 4 | You submit the signed transaction | `POST /v1/treasury/rebalances/{rebalanceId}/steps/{stepId}/sign` with `{ "signature": "0x..." }`; `step.signed` fires; `rebalance.balance_updated` with `balance_status: "reserved"` once the signed step is accepted | `balance_status`, `balance_reserved_at`; step `status: "signed"` | | 5 | Tesser broadcasts the signed transaction | `step.submitted` on the step | Step 1: `submitted_at`, `transaction_hash` | | 6 | The on-chain transaction is visible on the network | `step.confirmed` on the step | Step 1: `confirmed_at` | | 7 | The transfer reaches finality; rebalance is complete | `step.completed` on the step, followed by `rebalance.updated` with the terminal Rebalance object | Step 1: `completed_at`, `actual.from.amount`, `actual.to.amount`. Rebalance-level `actual.from.amount` and `actual.to.amount`. | ## Failure Modes for Rebalances If a rebalance fails, the top-level `actual.*` fields populate only when at least one step has reached `step.status = completed`. With one or more completed steps, the top-level `actual.from` matches the first completed step's `actual.from`, and the top-level `actual.to` matches the last completed step's `actual.to`. With no completed step, the top-level `actual.*` stays all-null — `desired.*` and `estimated.*` describe the originally requested and quoted state, while step-level `status_reasons` carries the cause of the failure. Failed steps always have all-null `actual.*`. Any steps subsequent to the failure step also transition to `failed` with null `actual.*` fields. ### Circle-Internal Transfer Rejected (Scenario 1) If Circle rejects the internal move between your two Circle ledger accounts, the single `transfer` step transitions directly to `failed` (no intermediate `submitted` or `confirmed` lifecycle is observed for Circle-internal moves). Funds remain at the `desired.from.account_id`. What you will observe: - `step.failed` on the single Circle-internal step. `actual.*` is null because no funds moved. - The rebalance's top-level `actual.*` is null for the same reason. `desired.*` is preserved. - A `rebalance.updated` webhook fires alongside the terminal `step.failed`, carrying the full updated Rebalance object with the populated terminal state. Example `step.failed` webhook (Scenario 1, Circle-internal step rejected): ```json { "id": "fa1a1b2c-d4e5-4ff0-90ab-aa34567890ab", "type": "step.failed", "created_at": "2025-12-01T10:30:00.000Z", "data": { "object": { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "failed", "status_reasons": [ { "error_code": "rebalances-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:30:00.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T10:30:00.000Z" } } } ``` Example `rebalance.updated` webhook (Scenario 1, Circle-internal rejected): ```json { "id": "ea1a1b2c-d4e5-4fe0-90ab-bb34567890ab", "type": "rebalance.updated", "created_at": "2025-12-01T10:30:00.200Z", "data": { "object": { "id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "failed", "status_reasons": [ { "error_code": "rebalances-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:30:00.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T10:30:00.000Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:30:00.200Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ### Trade Fails to Complete (OpenFX) (Scenario 2a) If Tesser attempts the swap at the OpenFX ledger and cannot get the trade to succeed before the rebalance's `expires_at` timestamp, the rebalance terminates with USD still at the OpenFX ledger. The conversion into USDC did not happen, and the subsequent on-chain transfer step is never submitted. What you will observe: - `step.failed` on Step 1 (swap). The step's `actual.*` is null because the swap never completed; failure details are in `status_reasons`. - `step.failed` on Step 2 (wallet transfer). This step never entered `submitted`, because there were no USDC to transfer. - The rebalance's top-level `actual.*` is all null because no step reached `step.status = completed` — the swap step failed, and Step 2 (wallet transfer) never started. `desired.to` stays as `USDC`/`BASE` at the wallet (client intent is never overwritten); `desired.from` and `estimated.*` describe what was requested and planned. - A `rebalance.updated` webhook fires alongside the terminal `step.failed` events, carrying the full updated Rebalance object with the populated `actual.*` overlay. Example `step.failed` webhook on Step 1 (swap, never filled): ```json { "id": "fa2a2bb2-2222-4ff2-8222-bb22222222a2", "type": "step.failed", "created_at": "2025-12-01T14:00:00.000Z", "data": { "object": { "id": "4d6f7a8b-1c2d-4e5f-8a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "rebalances-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": "2025-12-01T10:32:18.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `step.failed` webhook on Step 2 (wallet transfer, never submitted): ```json { "id": "fa3b3cc3-3333-4ff3-8333-cc33333333a3", "type": "step.failed", "created_at": "2025-12-01T14:00:00.100Z", "data": { "object": { "id": "5e7f8a9b-2c3d-4e5f-9a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "rebalances-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.100Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.100Z" } } } ``` Example `rebalance.updated` webhook (Scenario 2a, swap never filled — USD remains at the OpenFX ledger): ```json { "id": "ea2c4e6f-8a0b-4c2d-9e3f-4a5b6c7d8e9f", "type": "rebalance.updated", "created_at": "2025-12-01T14:00:00.300Z", "data": { "object": { "id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": null, "currency": "USDC", "network": "BASE" } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "4d6f7a8b-1c2d-4e5f-8a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "rebalances-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": "2025-12-01T10:32:18.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" }, { "id": "5e7f8a9b-2c3d-4e5f-9a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "rebalances-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.100Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.100Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.300Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ### Wallet Transfer Fails Post-Swap (Scenario 2b) If the swap at the OpenFX ledger succeeds but the on-chain transfer to the BASE wallet fails (e.g., the transaction reverts on-chain or cannot be confirmed before `expires_at`), the rebalance terminates with USDC sitting at the OpenFX ledger. The swap step is `completed` with the actual fill amount; the transfer step is `failed`. What you will observe: - `step.completed` on Step 1 (swap). `actual.to.amount` reflects the realized fill (`999.475` USDC). - `step.failed` on Step 2 (wallet transfer). `actual.*` is null because no on-chain transfer was confirmed. - The rebalance's top-level `actual.from` reflects the `desired.from.amount` and `desired.from.currency` (`1000` USD). `actual.to` resolves to USDC at the OpenFX ledger account `2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b` with amount `999.475`. The USDC that resulted from the swap is available to use for a future (new) rebalance. - A `rebalance.updated` webhook fires alongside the terminal `step.failed` event. Example `step.failed` webhook on Step 2 (wallet transfer, post-swap failure): ```json { "id": "fb4c5d6e-7f8a-4b9c-9d0e-1f2a3b4c5d6e", "type": "step.failed", "created_at": "2025-12-01T11:00:00.000Z", "data": { "object": { "id": "5e7f8a9b-2c3d-4e5f-9a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "rebalances-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:00:00.000Z", "submitted_at": "2025-12-01T10:35:05.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T11:00:00.000Z" } } } ``` Example `rebalance.updated` webhook (Scenario 2b, USDC stuck at OpenFX ledger): ```json { "id": "eb5d7f9a-1c3e-4b5d-8f0a-2c4e6f8a0b1c", "type": "rebalance.updated", "created_at": "2025-12-01T11:00:00.200Z", "data": { "object": { "id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "reserved", "balance_reserved_at": "2025-12-01T10:00:01.000Z", "desired": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": null, "currency": "USDC", "network": "BASE" } }, "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USDC", "network": null } }, "steps": [ { "id": "4d6f7a8b-1c2d-4e5f-8a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 1, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USDC", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:33:00.500Z", "submitted_at": "2025-12-01T10:33:00.100Z", "confirmed_at": "2025-12-01T10:33:00.300Z", "completed_at": "2025-12-01T10:33:00.500Z", "failed_at": null }, { "id": "5e7f8a9b-2c3d-4e5f-9a7b-8c9d0e1f2a3b", "transfer_id": "8b2c3d4e-5f6a-4890-9bcd-2345678901bc", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDC", "network": "BASE" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDC", "network": "BASE" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "rebalances-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:00:00.000Z", "submitted_at": "2025-12-01T10:35:05.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T11:00:00.000Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T11:00:00.200Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ### Insufficient Funds (Scenario 1 illustration) If the `desired.from.account_id` does not have enough balance to cover `desired.from.amount`, the balance check returns `awaiting_funds` instead of `reserved`. Tesser fires `rebalance.balance_updated` with `balance_status: "awaiting_funds"` and queues the rebalance, retrying the reservation as the balance at `desired.from.account_id` changes (e.g., as a deposit lands or another rebalance frees funds). If the reservation does not succeed before `expires_at`, the rebalance times out: every step transitions to `failed`, `step.failed` events fire for each, and a final `rebalance.updated` webhook reports the terminal state with `actual.*` null because no funds moved. What you will observe: - `rebalance.balance_updated` with `balance_status: "awaiting_funds"` shortly after creation. - (Optional) further `rebalance.balance_updated` events as balance changes are detected. - At `expires_at`: `step.failed` for every step, followed by `rebalance.updated` with the terminal Rebalance object. Example `rebalance.balance_updated` webhook (Scenario 1, source ledger short): ```json { "id": "ac1b3d5e-7f9a-4b2c-8d4e-6f8a0b2c4d6e", "type": "rebalance.balance_updated", "created_at": "2025-12-01T10:00:01.000Z", "data": { "object": { "id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "awaiting_funds", "balance_reserved_at": null, "desired": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:01.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `rebalance.updated` webhook (Scenario 1, timed out — funds at `desired.from.account_id` never replenished): ```json { "id": "ed6f8a0b-2c4e-4f6a-9b0c-3d5f7a9b1c3e", "type": "rebalance.updated", "created_at": "2025-12-01T14:00:00.500Z", "data": { "object": { "id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "rebalance", "balance_status": "awaiting_funds", "balance_reserved_at": null, "desired": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "c1c1e7d1-1aaa-4f01-b001-aaaa11110001", "amount": "1000", "currency": "USDC", "network": null }, "to": { "account_id": "c2c2f8d2-2bbb-4f02-b002-bbbb22220002", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "failed", "status_reasons": [ { "error_code": "rebalances-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.500Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` --- ## Document: Deposit Funds via a Liquidity Provider Deposit fiat funds via a liquidity provider, with or without on-ramping to stablecoin. URL: /how-tos/deposit-funds-via-a-liquidity-provider # Deposit Funds via a Liquidity Provider 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](/overviews/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](/how-tos/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](/overviews/funds-movement-lifecycle-and-data-model#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. :::note If you choose to pre-fund, you will create a deposit for the transfer of fiat to the provider and then a [rebalance](/api/treasury#create-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](#registering-your-bank-accounts-and-wallets-with-liquidity-providers)). - `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. :::note 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:** 1. Depositing and on-ramping to a ledger at Circle 2. Depositing fiat to a ledger at OpenFX 3. Depositing and on-ramping into a self-custodial wallet via OpenFX Example request (deposit and on-ramp to a ledger at Circle) ```json { "tenant_id": null, "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD" }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "currency": "USDC" } } } ``` Example request (deposit fiat to a ledger at OpenFX) ```json { "tenant_id": null, "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD" }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "currency": "USD" } } } ``` Example request (deposit and on-ramp to a wallet via OpenFX) ```json { "tenant_id": null, "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "currency": "USDT", "network": "POLYGON" } } } ``` In the API response, Tesser will create and return an `id` for the deposit request. Example response (deposit and on-ramp to a ledger at Circle): ```json { "data": { "id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example response (deposit fiat to a ledger at OpenFX): ```json { "data": { "id": "3c5d6f8e-0a1b-4c2d-9e3f-4a5b6c7d8e9f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` Example response (deposit and on-ramp to a wallet via OpenFX): ```json { "data": { "id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": null, "currency": "USDT", "network": "POLYGON" } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } ``` ## Deposit Quote Created (`deposit.quote_created`) 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): ```json { "id": "b8c4d5e6-2f3a-4b7c-9d8e-1f2a3b4c5d6e", "type": "deposit.quote_created", "created_at": "2025-12-01T10:00:00.600Z", "data": { "object": { "id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:00.600Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `deposit.quote_created` webhook (deposit fiat to a ledger at OpenFX): ```json { "id": "e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b", "type": "deposit.quote_created", "created_at": "2025-12-01T10:00:02.000Z", "data": { "object": { "id": "3c5d6f8e-0a1b-4c2d-9e3f-4a5b6c7d8e9f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "4d6f7a8b-1c2d-4e5f-6a7b-8c9d0e1f2a3b", "transfer_id": "3c5d6f8e-0a1b-4c2d-9e3f-4a5b6c7d8e9f", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:02.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "5e7f8a9b-2c3d-4e5f-6a7b-8c9d0e1f2a3b", "transfer_id": "3c5d6f8e-0a1b-4c2d-9e3f-4a5b6c7d8e9f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:02.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:02.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `deposit.quote_created` webhook (deposit and on-ramp to a wallet via OpenFX): ```json { "id": "e47a9b21-5c4d-4f82-9a13-7b8e3d1c5f04", "type": "deposit.quote_created", "created_at": "2025-12-01T10:00:02.000Z", "data": { "object": { "id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": null, "currency": "USDT", "network": "POLYGON" } }, "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:02.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:02.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "5c7a8e4d-1b9f-4c62-a8e3-7d6f3b4a2c9e", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 3, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDT", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:02.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "9d3f6a2c-8e5b-4f17-b0a6-1c4e7d9f3b8a", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 4, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "created", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:02.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T10:00:02.000Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ## 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](/api/accounts#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. :::warning{title="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): ```json { "id": "f1e2d3c4-5b6a-4798-8e0d-3f4a5b6c7d8e", "type": "step.completed", "created_at": "2025-12-02T14:30:00.500Z", "data": { "object": { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:30:00.500Z", "submitted_at": "2025-12-02T14:30:00.300Z", "confirmed_at": "2025-12-02T14:30:00.400Z", "completed_at": "2025-12-02T14:30:00.500Z", "failed_at": null } } } ``` :::note For deposits into a ledger at Circle, `transaction_hash` is always `null`. ::: Example `deposit.updated` webhook (deposit and on-ramp to a ledger at Circle complete): ```json { "id": "e2c6d8b1-4f7a-4e93-9b8d-2a5c7f3e1d4b", "type": "deposit.updated", "created_at": "2025-12-02T14:30:00.700Z", "data": { "object": { "id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:25:00.500Z", "submitted_at": "2025-12-02T14:25:00.300Z", "confirmed_at": "2025-12-02T14:25:00.400Z", "completed_at": "2025-12-02T14:25:00.500Z", "failed_at": null }, { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:30:00.500Z", "submitted_at": "2025-12-02T14:30:00.300Z", "confirmed_at": "2025-12-02T14:30:00.400Z", "completed_at": "2025-12-02T14:30:00.500Z", "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:30:00.700Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `step.completed` webhook (terminal step for deposit fiat to a ledger at OpenFX): ```json { "id": "f3a4b5c6-7d8e-4f9a-0b1c-2d3e4f5a6b7c", "type": "step.completed", "created_at": "2025-12-02T14:30:00.500Z", "data": { "object": { "id": "5e7f8a9b-2c3d-4e5f-6a7b-8c9d0e1f2a3b", "transfer_id": "3c5d6f8e-0a1b-4c2d-9e3f-4a5b6c7d8e9f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:30:00.500Z", "submitted_at": "2025-12-02T14:30:00.300Z", "confirmed_at": "2025-12-02T14:30:00.400Z", "completed_at": "2025-12-02T14:30:00.500Z", "failed_at": null } } } ``` Example `deposit.updated` webhook (deposit fiat to a ledger at OpenFX complete): ```json { "id": "a4b5c6d7-8e9f-4a0b-1c2d-3e4f5a6b7c8d", "type": "deposit.updated", "created_at": "2025-12-02T14:30:00.700Z", "data": { "object": { "id": "3c5d6f8e-0a1b-4c2d-9e3f-4a5b6c7d8e9f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": null, "currency": "USD", "network": null } }, "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "steps": [ { "id": "4d6f7a8b-1c2d-4e5f-6a7b-8c9d0e1f2a3b", "transfer_id": "3c5d6f8e-0a1b-4c2d-9e3f-4a5b6c7d8e9f", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:25:00.500Z", "submitted_at": "2025-12-02T14:25:00.300Z", "confirmed_at": "2025-12-02T14:25:00.400Z", "completed_at": "2025-12-02T14:25:00.500Z", "failed_at": null }, { "id": "5e7f8a9b-2c3d-4e5f-6a7b-8c9d0e1f2a3b", "transfer_id": "3c5d6f8e-0a1b-4c2d-9e3f-4a5b6c7d8e9f", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:30:00.500Z", "submitted_at": "2025-12-02T14:30:00.300Z", "confirmed_at": "2025-12-02T14:30:00.400Z", "completed_at": "2025-12-02T14:30:00.500Z", "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:30:00.700Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` Example `step.completed` webhook (terminal step for deposit and on-ramp to a wallet via OpenFX): ```json { "id": "c2e5f3d1-7b9a-4c48-8e0d-3f4a5b6c7d8e", "type": "step.completed", "created_at": "2025-12-02T14:36:35.000Z", "data": { "object": { "id": "9d3f6a2c-8e5b-4f17-b0a6-1c4e7d9f3b8a", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 4, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.475", "currency": "USDT", "network": "POLYGON" } }, "fees": [], "transaction_hash": "0x7e8c3f2a1b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f", "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:36:35.000Z", "submitted_at": "2025-12-02T14:35:05.000Z", "confirmed_at": "2025-12-02T14:35:07.500Z", "completed_at": "2025-12-02T14:36:35.000Z", "failed_at": null } } } ``` Example `deposit.updated` webhook (deposit and on-ramp to a wallet via OpenFX complete): ```json { "id": "b8d3a5f7-2c4e-4691-9d3a-8f6b1e7c4d52", "type": "deposit.updated", "created_at": "2025-12-02T14:36:35.200Z", "data": { "object": { "id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": null, "currency": "USDT", "network": "POLYGON" } }, "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.475", "currency": "USDT", "network": "POLYGON" } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:25:00.500Z", "submitted_at": "2025-12-02T14:25:00.300Z", "confirmed_at": "2025-12-02T14:25:00.400Z", "completed_at": "2025-12-02T14:25:00.500Z", "failed_at": null }, { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:30:00.500Z", "submitted_at": "2025-12-02T14:30:00.300Z", "confirmed_at": "2025-12-02T14:30:00.400Z", "completed_at": "2025-12-02T14:30:00.500Z", "failed_at": null }, { "id": "5c7a8e4d-1b9f-4c62-a8e3-7d6f3b4a2c9e", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 3, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDT", "network": null } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USDT", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:33:00.500Z", "submitted_at": "2025-12-02T14:33:00.100Z", "confirmed_at": "2025-12-02T14:33:00.300Z", "completed_at": "2025-12-02T14:33:00.500Z", "failed_at": null }, { "id": "9d3f6a2c-8e5b-4f17-b0a6-1c4e7d9f3b8a", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 4, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.475", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.475", "currency": "USDT", "network": "POLYGON" } }, "fees": [], "transaction_hash": "0x7e8c3f2a1b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f", "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:36:35.000Z", "submitted_at": "2025-12-02T14:35:05.000Z", "confirmed_at": "2025-12-02T14:35:07.500Z", "completed_at": "2025-12-02T14:36:35.000Z", "failed_at": null } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:36:35.200Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ## Webhook Events by Scenario 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. | :::note 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. | :::note 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 | Step 4: `completed_at`, `actual.to.amount`. Deposit-level `actual.to.amount`. | :::note Lifecycle events for Steps 1, 2, and 3 will arrive in close succession. ::: ## Failure Modes for Deposits If a deposit fails, the top-level `actual.*` fields populate only when at least one step has reached `step.status = completed`. With one or more completed steps, the top-level `actual.from` matches the first completed step's `actual.from`, and the top-level `actual.to` matches the last completed step's `actual.to`. With no completed step, the top-level `actual.*` stays all-null — `desired.*` and `estimated.*` describe the originally requested and quoted state, while step-level `status_reasons` carries the cause of the failure. Failed steps always have all-null `actual.*`. Any steps subsequent to the failure step also transition to `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 (step 3 in scenario 3). Step-level `actual.*` is null; `status_reasons` carries the failure detail. - `step.failed` on the subsequent wallet-transfer step (step 4 in scenario 3). This step never entered `submitted` because there were no stablecoins to withdraw; step-level `actual.*` is null, and `status_reasons` carries the failure detail. - The deposit's top-level `actual.from` matches the first completed step's `actual.from` (the original fiat source), and `actual.to` matches the last completed step's `actual.to` — the OpenFX ledger holding the source USD. `desired.to.currency` stays as the originally requested stablecoin (client intent is never overwritten). For a USD→USDT deposit, the terminal top-level shows `actual.to.currency: "USD"` and `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. :::note The JSON examples below show steps 3 and 4 from scenario 3. ::: Example webhook schema for `step.failed` on step 3 (swap): ```json { "id": "d8a4f2c1-6e3b-4f59-a7c0-1d2e3f4a5b6c", "type": "step.failed", "created_at": "2025-12-03T04:00:00.000Z", "data": { "object": { "id": "5c7a8e4d-1b9f-4c62-a8e3-7d6f3b4a2c9e", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 3, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDT", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "deposits-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-03T04:00:00.000Z", "submitted_at": "2025-12-02T14:32:18.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-03T04:00:00.000Z" } } } ``` Example webhook schema for `step.failed` on step 4 (wallet transfer, never submitted): ```json { "id": "e4b5f3d2-8c91-4a7b-b3e5-0d1f2a3b4c5d", "type": "step.failed", "created_at": "2025-12-03T04:00:00.100Z", "data": { "object": { "id": "9d3f6a2c-8e5b-4f17-b0a6-1c4e7d9f3b8a", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 4, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "deposits-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-03T04:00:00.100Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-03T04:00:00.100Z" } } } ``` Example `deposit.updated` webhook (deposit terminated as fiat after trade give-up): ```json { "id": "c5f3a2d8-7b4e-4192-9d6c-3e8a1f2b5c7d", "type": "deposit.updated", "created_at": "2025-12-03T04:00:00.300Z", "data": { "object": { "id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": null, "currency": "USDT", "network": "POLYGON" } }, "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:25:00.500Z", "submitted_at": "2025-12-02T14:25:00.300Z", "confirmed_at": "2025-12-02T14:25:00.400Z", "completed_at": "2025-12-02T14:25:00.500Z", "failed_at": null }, { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-02T14:30:00.500Z", "submitted_at": "2025-12-02T14:30:00.300Z", "confirmed_at": "2025-12-02T14:30:00.400Z", "completed_at": "2025-12-02T14:30:00.500Z", "failed_at": null }, { "id": "5c7a8e4d-1b9f-4c62-a8e3-7d6f3b4a2c9e", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 3, "step_type": "swap", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDT", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "deposits-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-03T04:00:00.000Z", "submitted_at": "2025-12-02T14:32:18.000Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-03T04:00:00.000Z" }, { "id": "9d3f6a2c-8e5b-4f17-b0a6-1c4e7d9f3b8a", "transfer_id": "2b4c5e7a-9f3d-41b8-8c6a-7e5f9d2a1c3b", "step_sequence": 4, "step_type": "transfer", "estimated": { "from": { "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b", "amount": "999.525", "currency": "USDT", "network": "POLYGON" }, "to": { "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f", "amount": "999.525", "currency": "USDT", "network": "POLYGON" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "openfx", "status": "failed", "status_reasons": [ { "error_code": "deposits-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-03T04:00:00.100Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-03T04:00:00.100Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-03T04:00:00.300Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` ### 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): ```json { "id": "f6a8c4e2-3b9d-4f15-a7c8-5d2e9b1f3a4c", "type": "deposit.updated", "created_at": "2025-12-01T14:00:00.500Z", "data": { "object": { "id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "organization_reference_id": null, "direction": "inbound", "desired": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": null, "currency": "USDC", "network": null } }, "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "steps": [ { "id": "8a4f2c1e-9b6d-4e35-b7a0-3c5d1e9f2b8a", "transfer_id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "55042f8f-e527-56f1-b57c-eef23ca862db", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "failed", "status_reasons": [ { "error_code": "deposits-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" }, { "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b", "transfer_id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "a81bc1f4-7e3d-4926-b04f-3d2e8a9c5f17", "amount": "1000", "currency": "USD", "network": null }, "to": { "account_id": "44031e7e-d416-45f0-a46b-ded12b9751ca", "amount": "1000", "currency": "USDC", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "fees": [], "transaction_hash": null, "provider_key": "circle_mint", "status": "failed", "status_reasons": [ { "error_code": "deposits-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T14:00:00.000Z" } ], "created_at": "2025-12-01T10:00:00.000Z", "updated_at": "2025-12-01T14:00:00.500Z", "expires_at": "2025-12-01T14:00:00.000Z" } } } ``` --- ## Document: Create an Account Pre-register accounts for payouts on the Tesser platform URL: /how-tos/create-an-account # Create an Account Accounts must be pre-registered to transact on the Tesser platform. Registering accounts in advance of transacting enables wallet risk assessment and helps meet regulatory requirements. **Account creation** To create an account, submit a request to create a bank, wallet, or ledger account and populate the appropriate fields. Accounts will always belong to the workspace they are created in and can also optionally belong to a tenant or counterparty. See the [Accounts overview](/overviews/accounts) for more details. Bank account creation request ```json { "counterparty_id": "2a7e1c9f-4b3d-4a82-b6e0-8f5c2d1a9b3e", "name": "Operating Account - EU", "bank_name": "Chase Bank", "bank_code_type": "SWIFT", "bank_identifier_code": "CHASUS33", "bank_account_number": "123456789" } ``` The response will provide a unique identifier for the account that can be used when requesting payments or transfers: ```json { "data": { "id": "8f3b1e7c-9a2d-4f58-b6c0-4e7d2a9f1b3c", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "tenant_id": null, "counterparty_id": "2a7e1c9f-4b3d-4a82-b6e0-8f5c2d1a9b3e", "type": "fiat_bank", "name": "Operating Account - EU", "bank_name": "Chase Bank", "bank_code_type": "SWIFT", "bank_identifier_code": "CHASUS33", "crypto_wallet_address": null, "created_at": "2024-03-02T14:30:00.000Z", "updated_at": "2024-03-02T14:30:00.000Z" } } ``` Wallet account creation request ```json { "workspace_id": "b53f6690-3242-4942-9907-885779632832", "name": "Treasury Wallet (Omnibus)", "type": "stablecoin_ethereum", "signature": "eyJwdWJsaWNLZXkiOiJwdWJfZXhhbXBsZSIsInNpZ25hdHVyZSI6InNpZ19leGFtcGxlIiwic2NoZW1lIjoiRUQyNTUxOSJ9" } ``` The response will provide a unique identifier for the account that can be used when requesting payments or transfers: ```json { "data": { "id": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "tenant_id": null, "counterparty_id": null, "type": "stablecoin_ethereum", "name": "Treasury Wallet (Omnibus)", "bank_name": null, "bank_code_type": null, "bank_identifier_code": null, "crypto_wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE0B", "created_at": "2024-03-01T10:00:00.000Z", "updated_at": "2024-03-01T10:00:00.000Z" } } ``` Ledger account creation request ```json { "workspace_id": "b53f6690-3242-4942-9907-885779632832", "name": "Treasury Ledger", "provider": "CIRCLE_MINT" } ``` The response will provide a unique identifier for the account that can be used when requesting payments or transfers: ```json { "data": { "id": "5d2e8f1a-7c3b-4a69-9e0f-1b4c6d8a2e5f", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "tenant_id": null, "counterparty_id": null, "name": "Treasury Ledger", "type": "ledger", "provider": "CIRCLE_MINT" } } ``` **Associating wallets and ledgers with a tenant or counterparty** A wallet or ledger can optionally belong to either a counterparty or tenant in addition to the workspace. - Do not specify a tenant or counterparty if you want the wallet or ledger to be used in a fully omnibus manner, where funds are commingled across all of your customers. Upon onboarding, your organization will be provisioned with a wallet or ledger that only belongs to your workspace. You may create additional wallets or ledgers that only belong to the workspace as needed. - Create a wallet or ledger that belongs to a tenant to manage proprietary funds belonging to that tenant, or to commingle funds belonging to that tenant's customers (e.g. originators and/or beneficiaries). - Create a wallet or ledger that belongs to a counterparty to fully segregate funds only for that counterparty. For more information on the relationship between accounts and counterparties, see the [Accounts overview](/overviews/accounts). --- ## Document: Create a Tenant How to create and manage tenants for platform customers URL: /how-tos/create-a-tenant # Create a Tenant A tenant represents an organization’s customer when that customer is itself a platform with its own users who need to be represented in Tesser’s systems. When an organization’s customers are tenants, originators and/or beneficiaries are customers of the tenant and thus end-customers to the organization. To create a tenant, submit a request to the [tenant creation API](/api/tenants#create-a-new-tenant). ```json { "business_legal_name": "Acme Corporation", "business_dba": "Acme Co", "business_address_country": "US", "business_street_address1": "123 Corp Blvd", "business_street_address2": "Suite 100", "business_city": "New York", "business_state": "NY", "business_legal_entity_identifier": "123456789", "webhook_url": "https://acme.com/webhooks/tesser" } ``` Example response: ```json { "data": { "id": "29c580bd-43d2-4dbf-bc9e-a3b1ccb8e7ee", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "business_legal_name": "Acme Corporation", "business_dba": "Acme Co", "business_address_country": "US", "business_street_address1": "123 Corp Blvd", "business_street_address2": "Suite 100", "business_city": "New York", "business_state": "NY", "business_legal_entity_identifier": "9876543210", "country": "US", "webhook_url": "https://acme.com/webhooks/tesser", "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:30:00.000Z" } } ``` **Creating resources for a tenant** To initiate counterparty, account, payment, or transfer creation on behalf of the tenant, you will supply the `id` of the tenant returned in the creation response in API calls using your own API keys. --- ## Document: Create a Counterparty Pre-register counterparties for payouts on the Tesser platform URL: /how-tos/create-a-counterparty # Create a Counterparty Counterparties must be pre-registered to transact on the Tesser platform. Registering counterparties in advance of submitting payouts enables OFAC (sanctions) screening and helps meet regulatory requirements. **Counterparty creation** To create a counterparty, submit a request to the [counterparty creation API](/api/counterparties#create-a-new-counterparty) and populate the appropriate fields for the individual or business. *Note: Individuals are natural persons; businesses are registered legal entities. Select a type of customer you want to create: Example: Individual counterparty creation request ```json { "classification": "individual", "individual_first_name": "John", "individual_last_name": "Doe", "individual_address_country": "US", "individual_date_of_birth": "1980-01-01", "individual_national_identification_number": "123456789", "individual_street_address1": "123 Main St", "individual_street_address2": "Apt 4B", "individual_city": "New York", "individual_state": "NY", "individual_postal_code": "10001" } ``` In the successful response you receive a unique id for the counterparty. ```json { "data": { "id": "c7d8e9f0-1a2b-3c4d-5e6f-789012345678", "workspace_id": "b53f6690-3242-4942-9907-885779632832", "classification": "individual", "tenant_id": null, "individual_first_name": "John", "individual_last_name": "Doe", "individual_address_country": "US", "individual_date_of_birth": "1980-01-01", "individual_national_identification_number": "123456789", "individual_street_address1": "123 Main St", "individual_street_address2": "Apt 4B", "individual_city": "New York", "individual_state": "NY", "individual_postal_code": "10001", "business_legal_name": null, "business_dba": null, "business_address_country": null, "business_street_address1": null, "business_street_address2": null, "business_city": null, "business_state": null, "business_legal_entity_identifier": null, "created_at": "2024-03-01T10:00:00.000Z", "updated_at": "2024-03-01T10:00:00.000Z" } } ``` Example: Business counterparty creation request ```json { "classification": "business", "business_legal_name": "Acme Corporation", "business_dba": "Acme Co", "business_address_country": "US", "business_street_address1": "456 Corp Blvd", "business_street_address2": "Suite 200", "business_city": "San Francisco", "business_state": "CA", "business_legal_entity_identifier": "1234567890" } ``` In the successful response you receive a unique id for the counterparty. ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "classification": "business", "tenant_id": "00000000-0000-0000-0000-000000000000", "individual_first_name": null, "individual_last_name": null, "individual_street_address1": null, "individual_street_address2": null, "individual_city": null, "individual_state": null, "individual_postal_code": null, "individual_address_country": null, "individual_date_of_birth": null, "individual_national_identification_number": null, "business_legal_name": "Acme Corporation", "business_dba": "Acme Co", "business_address_country": "US", "business_street_address1": "456 Corp Blvd", "business_street_address2": "Suite 200", "business_city": "San Francisco", "business_state": "CA", "business_legal_entity_identifier": "1234567890", "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:30:00.000Z" } } ``` --- ## Document: Resources AI-friendly resources and authentication for the Tesser API URL: /agentic/resources # Resources The Tesser API is designed for both human developers and AI agents. All endpoints are accessible via standard REST calls and via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), allowing AI coding assistants to discover and invoke Tesser operations as tools. ## AI-Friendly Resources | Resource | URL | | --- | --- | | **llms.txt** | [https://docs.tesser.xyz/llms.txt](https://docs.tesser.xyz/llms.txt) | | **Full docs for LLMs** | [https://docs.tesser.xyz/llms-full.txt](https://docs.tesser.xyz/llms-full.txt) | | **OpenAPI Schema** | [https://docs.tesser.xyz/api/v1/schema.json](https://docs.tesser.xyz/api/v1/schema.json) | | **MCP Endpoint** | `https://sandbox.tesserx.co/v1/mcp` | ## Authentication Both REST and MCP use the same OAuth 2.0 client credentials flow. You need a `TESSER_API_KEY` and `TESSER_API_SECRET` (issued as Auth0 client credentials). ### Obtain a Token ```bash curl --request POST \ --url https://dev-awqy75wdabpsnsvu.us.auth0.com/oauth/token \ --header 'content-type: application/json' \ --data '{ "client_id": "'$TESSER_API_KEY'", "client_secret": "'$TESSER_API_SECRET'", "audience": "https://sandbox.tesserx.co", "grant_type": "client_credentials" }' ``` The response contains an `access_token` to use as a Bearer token in all subsequent requests. ### Environment Setup Create a `.env` file in your project root: ```bash TESSER_API_KEY=your-api-key TESSER_API_SECRET=your-api-secret ``` --- ## Document: MCP Integration Connect AI coding assistants to Tesser via Model Context Protocol URL: /agentic/mcp-integration # MCP Integration The Tesser MCP server exposes every public API endpoint as a tool that AI agents can discover and invoke. It uses **Streamable HTTP** transport at: ``` https://sandbox.tesserx.co/v1/mcp ``` Authentication uses the same OAuth 2.0 Bearer token as the REST API. See [Resources](/agentic/resources) for how to obtain a token. ## Claude Code **1. Set your token** in the shell before launching Claude Code: ```bash export MCP_TOKEN=$(bash scripts/mcp-token.sh) ``` **2. Add the config** to `.claude/settings.local.json` in your project root: ```jsonc { "mcpServers": { "tesser-payments": { "type": "url", "url": "https://sandbox.tesserx.co/v1/mcp", "headers": { "Authorization": "Bearer ${MCP_TOKEN}" } } } } ``` **3. Activate** by running `/mcp` in your Claude Code session to reload MCP servers. Tesser tools will appear immediately. ## Cursor **1. Set your token** in the shell before launching Cursor: ```bash export MCP_TOKEN=$(bash scripts/mcp-token.sh) ``` **2. Add the config** to `.cursor/mcp.json` in your project root: ```jsonc { "mcpServers": { "tesser-payments": { "type": "url", "url": "https://sandbox.tesserx.co/v1/mcp", "headers": { "Authorization": "Bearer ${MCP_TOKEN}" } } } } ``` **3. Activate** by restarting Cursor. Tesser tools will appear in the MCP tools panel. ## Windsurf **1. Set your token** in the shell before launching Windsurf: ```bash export MCP_TOKEN=$(bash scripts/mcp-token.sh) ``` **2. Add the config** to `.windsurf/mcp.json` in your project root: ```jsonc { "mcpServers": { "tesser-payments": { "type": "url", "url": "https://sandbox.tesserx.co/v1/mcp", "headers": { "Authorization": "Bearer ${MCP_TOKEN}" } } } } ``` **3. Activate** by restarting Windsurf. ## Token Refresh Tokens expire after approximately 24 hours. When they expire, MCP tool calls will fail with a 401 error. To refresh: 1. Run `export MCP_TOKEN=$(bash scripts/mcp-token.sh)` in your shell 2. Restart your IDE (or run `/mcp` in Claude Code) For details on the token script and manual token management, see [Manual MCP Setup](/agentic/manual-mcp). --- ## Document: Manual MCP Setup Connect to the Tesser MCP server using a manually fetched token URL: /agentic/manual-mcp # Manual MCP Setup Use this approach when `mcp-remote` is not available — for example in headless environments, CI pipelines, or custom agent frameworks. ## Fetch a Token ```bash export MCP_TOKEN=$(curl -s --request POST \ --url https://dev-awqy75wdabpsnsvu.us.auth0.com/oauth/token \ --header 'content-type: application/json' \ --data '{ "client_id": "'$TESSER_API_KEY'", "client_secret": "'$TESSER_API_SECRET'", "audience": "https://sandbox.tesserx.co", "grant_type": "client_credentials" }' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") ``` This requires `TESSER_API_KEY` and `TESSER_API_SECRET` set in your environment or `.env` file. ## IDE Configuration Configure your IDE with a static token. This works with any IDE that supports MCP URL-type servers: ```jsonc { "mcpServers": { "tesser-payments": { "type": "url", "url": "https://sandbox.tesserx.co/v1/mcp", "headers": { "Authorization": "Bearer $MCP_TOKEN" } } } } ``` Set `MCP_TOKEN` in your shell **before** launching the IDE. ## Token Helper Script The Tesser repo includes a helper script that reads credentials from `.env` and outputs a fresh token: ```bash export MCP_TOKEN=$(bash scripts/mcp-token.sh) ``` ## Token Expiry Tokens expire after approximately 24 hours. When a token expires, MCP tool calls will fail with a 401 error. You will need to re-run the token fetch command and restart your IDE session. For automatic token refresh, use [mcp-remote](/agentic/mcp-integration) instead. --- ## Document: AI Context File A ready-to-paste context file that gives AI assistants Tesser API knowledge URL: /agentic/ai-context # AI Context File Copy the markdown below into a `CLAUDE.md`, `.cursorrules`, or similar AI context file in your project. This gives your AI coding assistant the knowledge it needs to work with the Tesser API without re-reading the full documentation each session. ## Usage 1. Copy the content below 2. Save it as `CLAUDE.md` (for Claude Code), `.cursorrules` (for Cursor), or any context file your tool supports 3. Place it in your project root ## Context File ````markdown # Tesser Payments API ## Overview Tesser is a payments platform for stablecoin and fiat transfers. The API handles payment creation, wallet management, counterparty onboarding, compliance screening, and treasury operations. ## API Access - **Base URL**: `https://sandbox.tesserx.co/v1` - **Auth**: OAuth 2.0 client credentials → Bearer token - **Docs**: https://docs.tesser.xyz - **LLM docs**: https://docs.tesser.xyz/llms-full.txt - **OpenAPI Schema**: https://docs.tesser.xyz/api/v1/schema.json ### Authentication ```bash curl --request POST \ --url https://dev-awqy75wdabpsnsvu.us.auth0.com/oauth/token \ --header 'content-type: application/json' \ --data '{ "client_id": "'$TESSER_API_KEY'", "client_secret": "'$TESSER_API_SECRET'", "audience": "https://sandbox.tesserx.co", "grant_type": "client_credentials" }' ``` ### Environment Variables ``` TESSER_API_KEY=your-api-key TESSER_API_SECRET=your-api-secret ``` ## MCP Server All API operations are available as MCP tools at `https://sandbox.tesserx.co/v1/mcp`. ## Key Endpoints | Method | Path | Description | | --- | --- | --- | | POST | `/v1/payments` | Create a payment (payout, deposit) | | GET | `/v1/payments/{id}` | Get payment by ID | | PATCH | `/v1/payments/{id}` | Update payment with account info | | POST | `/v1/payments/{id}/risk-review` | Submit risk review decision | | GET | `/v1/accounts` | List accounts (wallets, ledgers, bank accounts) | | POST | `/v1/accounts` | Create an account | | GET | `/v1/entities/counterparties` | List counterparties | | POST | `/v1/entities/counterparties` | Create a counterparty | | GET | `/v1/currencies` | List supported currencies | | GET | `/v1/networks` | List supported blockchain networks | ## API Conventions - All request/response fields use **snake_case** - Amounts are strings (e.g. `"1000.50"`) - IDs are UUIDs - Timestamps are ISO 8601 (e.g. `"2025-12-01T09:00:00.000Z"`) - Webhook events follow the pattern `resource.action` (e.g. `payment.balance_updated`, `step.confirmed`) ```` --- ## Document: Request Supported Currencies and Networks Learn which currencies and payment networks are available URL: /how-tos/send-a-stablecoin-payout/supported-currencies-and-networks # Request Supported Currencies and Networks You can use the API to learn which currencies and payment networks are available. ### **Request available blockchain networks** Call the [available networks](/api/networks#get-available-networks) endpoint to retrieve list of all supported blockchain networks. Returns an array with network key and display name. **Shell** ```bash curl -H "Authorization: Bearer ${YOUR_API_KEY}" \ -X GET https://api.tesser.xyz/v1/networks ``` **Response** **JSON** ```json { "data": [ { "key": "ETHEREUM", "name": "Ethereum" }, { "key": "POLYGON", "name": "Polygon" }, { "key": "STELLAR", "name": "Stellar" }, { "key": "SOLANA", "name": "Solana" } ] } ``` ### **Request available currencies and networks** Call the [**available currencies**](/api/currencies#get-available-currencies) endpoint to receive a list of supported currencies (crypto and fiat) and which networks they are supported on. Returns an array with currency name, key, decimals, and network information. ```bash curl -H "Authorization: Bearer ${YOUR_API_KEY}" \ -X GET https://api.tesser.xyz/v1/currencies ``` **Response** ```json { "data": [ { "name": "US Dollar", "key": "USD", "decimals": 2, "chain": null }, { "name": "Circle USD", "key": "USDC", "decimals": 6, "chain": "ETHEREUM" }, { "name": "Tether USD", "key": "USDT", "decimals": 6, "chain": "ETHEREUM" }, { "name": "Circle USD", "key": "USDC", "decimals": 6, "chain": "POLYGON" }, { "name": "Tether USD", "key": "USDT", "decimals": 6, "chain": "POLYGON" }, { "name": "Circle USD", "key": "USDC", "decimals": 6, "chain": "STELLAR" }, { "name": "Tether USD", "key": "USDT", "decimals": 6, "chain": "STELLAR" }, { "name": "Circle USD", "key": "USDC", "decimals": 6, "chain": "SOLANA" }, { "name": "Tether USD", "key": "USDT", "decimals": 6, "chain": "SOLANA" } ] } ``` --- ## Document: Payments Workflow End-to-end payment execution flow URL: /how-tos/send-a-stablecoin-payout/payout-workflow # Payments Workflow Payments progress through a workflow that plans the payment and then executes a sequence of funds transfer steps. - **Payment Planning (prior to funds movement)** - Plans the route for the payment and obtains quote information. See [Payment planning](/overviews/payment-planning). - Screens beneficiary wallet risk - If manual risk review is required, a user with sufficient permissions can review the payment in the Tesser dashboard, or the decision can be programmatically delivered to Tesser via [API](/api/payments#submit-risk-review). - **Payment Execution (funds movement)** - Reserves available balance (if insufficient funds, will enqueue for future execution) - Uses the signing keys configured for the organization to authorize transfers (if using self-custodial model) - Requires explicit initiation/approval by the customer (or their configured signing automation) — Tesser cannot execute unilaterally - Results in funds actually moving on the specified blockchain network - If applicable, delivers fiat to the beneficiary **Example workflow for stablecoin payouts:** ![image.png](./payout-workflow1.png) **Example workflow for fiat payouts:** ![image.png](./payout-workflow2.png) **Statuses in the payment workflow** Certain components of a payment maintain their own status as the payment progresses through the workflow: - Risk review - Balance Check - Transfer steps > *Note: For transfer steps, it is possible for the next step to begin execution prior to a prior step completing. E.g. a transfer of fiat may begin prior to a transfer of crypto finalizing. **Risk statuses** Result of risk review performed on beneficiary wallet
**Status** **Terminal** **Webhook event type** **Description**
`unchecked` No

Not sent. All payments at creation have a risk status of `unchecked`

Beneficiary wallet identifier has not been supplied or risk check has not yet completed.
`awaiting_decision` No payment.risk_updated Beneficiary wallet has been risk screened and requires manual review to determine whether to send the payout.
`auto_approved` Yes payment.risk_updated Beneficiary wallet has been risk screened and automatically approved per your organization's policy.
`manually_approved` Yes payment.risk_updated Beneficiary wallet has been risk screened and has been manually reviewed and approved.
`auto_rejected` Yes payment.risk_updated Beneficiary wallet has been risk screened and automatically rejected per your organization's policy.
`manually_rejected` Yes payment.risk_updated Beneficiary wallet has been risk screened and has been manually reviewed and rejected.
**Balance statuses** Result of balance check for source wallet
**Status** **Terminal** **Webhook event type** **Description**
`unreserved` No

Not sent. All payments at creation have a balance status of `unreserved`

Source wallet id has not been supplied or the reserve operation has not yet completed.
`awaiting_funds` No payment.balance_updated The balance of the source wallet was checked and there are insufficient funds to process the payout. The payment is queued and awaiting funds from a deposit.
`reserved` Yes payment.balance_updated The balance of the source wallet was checked and funds were reserved to process the payout.
**Payment Steps** Each payment consists of multiple steps that track the money movement through the system: - **On-Network Transfer**: Transfer stablecoins on the underlying network - **Cross-Network Bridge**: Move assets between different networks (when needed) - **Token Swap**: Convert between different stablecoins or tokens (when needed) - **Fiat Conversion**: Convert stablecoins to fiat currency (when needed for off-ramped payments) Steps are tracked individually, allowing you to monitor the exact progress of each money movement operation. **Payment steps statuses**
**Status** **Terminal** **Webhook event type** **Description**
`created` No

Not sent. All steps at creation have a status of `created`

Tesser has created a record for this step.
`submitted` No step.submitted Tesser submitted the step information to the blockchain or fiat payment network
`confirmed` No step.confirmed The step was accepted by the operator of the payment network
`finalized` No step.finalized For crypto transfer steps, the block containing the transfer step has been finalized. The transfer step is now permanent and irreversible. How long it takes to finalize a transfer depends on the blockchain.
`completed` No step.completed

For fiat transfer steps, indicates the local payment network has delivered funds to the beneficiary.

*Note, some fiat payment networks may not provide formal confirmation of funds delivery, in which case funds are assumed to be delivered unless a `failed` status is indicated

`failed` Yes step.failed The transfer step was not successful; funds were not transferred from the `from_account` to the `to_account`
--- ## Document: Create a Payout (via Custodian) Create and execute payouts using third-party custodian accounts URL: /how-tos/send-a-stablecoin-payout/create-a-stablecoin-payout # Create a Payout (via Custodian) This guide is for payout creation when funds are custodied via a third-party custodian (e.g. Circle). Before creating a payout, review the [Funds Movement Lifecycle and Data Model](/overviews/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](/api/payments#create-payment). :::note 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](/api/payments#update-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](#update-payout-with-account-information). ::: :::warning{title="Payouts expire"} Every payout has an `expires_at` timestamp. If a payout doesn't complete by then — for example, the Circle ledger never reaches `reserved` because funds weren't replenished, or risk review wasn't decisioned in time — it cannot be resurrected. To retry, create a new payout. See [Expiration](/overviews/funds-movement-lifecycle-and-data-model#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](#update-payout-with-account-information). Example request for stablecoin payout creation: ```json { "desired": { "from": { "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "currency": "USDC", "network": "ETHEREUM" } }, "organization_reference_id": "ref_123" } ``` Example request for fiat payout creation: ```json { "desired": { "from": { "currency": "USDC", "network": "ETHEREUM" }, "to": { "amount": "1000", "currency": "MXN", "network": null } }, "organization_reference_id": "ref_123" } ``` :::note 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. Example response for stablecoin payout creation: ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": null, "desired": { "from": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } ``` Example response for fiat payout creation: ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": null, "desired": { "from": { "account_id": null, "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } ``` ## Quote and planning 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](/overviews/funds-movement-lifecycle-and-data-model#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. :::note 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 Circle ledger) and `estimated.to.account_id` on the last step (the final beneficiary) — also stay `null` until you PATCH accounts and Tesser updates the route plan (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): ```json { "id": "8472fb87-73b3-45ee-8020-a3496b4fc7a1", "type": "payment.quote_created", "created_at": "2025-12-01T09:00:00.045Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": null, "desired": { "from": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.040Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.040Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example `payment.quote_created` webhook for a fiat payout (cross-currency, two steps; step 1 is on-chain Circle → Alfred USDC ledger, step 2 is the Alfred fiat off-ramp): ```json { "id": "8472fb87-73b3-45ee-8020-a3496b4fc7a1", "type": "payment.quote_created", "created_at": "2025-12-01T09:00:00.045Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": null, "desired": { "from": { "account_id": null, "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": null, "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": null, "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.040Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": null, "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "alfred", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.040Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.040Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ## Update payout with account information If you did not supply account information at creation, submit a PATCH request to the [Payments API](/api/payments#update-payment) to populate: - `desired.from.account_id`: Ledger on the Tesser platform that funds will come from. Because this guide is about custodian payouts, ensure this is a managed ledger account (`type: "ledger"`) at a supported custodian (e.g. Circle). - `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. :::note 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](/overviews/compliance-and-risk#travel-rule). ::: Example PATCH request for stablecoin payout updated with accounts: ```json { "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012" } } } ``` Example PATCH request for fiat payout updated with accounts: ```json { "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047" } } } ``` 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](#payout-terminal-state).) Tesser also updates the route plan now that account ids are known: step-level `account_id` fields are populated and `provider_key` is set on each step. Example `payment.updated` webhook after PATCHing accounts on a stablecoin payout: ```json { "id": "1c9a4f6e-8b3d-4f23-a056-9d7c4b2e1f08", "type": "payment.updated", "created_at": "2025-12-01T09:00:00.205Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.200Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example `payment.updated` webhook after PATCHing accounts on a fiat payout: ```json { "id": "1c9a4f6e-8b3d-4f23-a056-9d7c4b2e1f08", "type": "payment.updated", "created_at": "2025-12-01T09:00:00.205Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "alfred", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.200Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ## Payout risk review 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](/overviews/funds-movement-lifecycle-and-data-model#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](/api/payments#submit-risk-review) 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`. ## Balance check If the payout is approved, Tesser checks the balance of the Circle ledger at `desired.from.account_id`. If sufficient funds are present, `balance_status` becomes `"reserved"` and `balance_reserved_at` is populated. The outcome is published on a `payment.balance_updated` webhook. See [Balance Statuses](/overviews/funds-movement-lifecycle-and-data-model#balance-statuses) on the lifecycle overview for the full taxonomy. If funds are insufficient, the payout enters `balance_status: "awaiting_funds"` and waits for funds to arrive (or for the payout to expire). See [Awaiting funds](#awaiting-funds) under Failure Modes for the resume-on-funding behavior. Example `payment.balance_updated` webhook for a stablecoin payout after a successful balance check: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "payment.balance_updated", "created_at": "2025-12-01T09:00:01.002Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:01.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example `payment.balance_updated` webhook for a fiat payout after a successful balance check: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "payment.balance_updated", "created_at": "2025-12-01T09:00:01.002Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "alfred", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:01.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ## On-chain processing After balance is reserved, Tesser instructs the custodian to broadcast the on-chain transfer step. The step's `status` transitions through `submitted` → `confirmed` → `completed` (each emits a `step.*` webhook — see [Execution](/overviews/funds-movement-lifecycle-and-data-model#execution)). The blockchain `transaction_hash` is populated on the step once submitted, and gas fees appear in the per-step `fees[]` array once confirmed. :::note Tesser sponsors gas fees for your organization, so on-chain transfers will not fail due to insufficient native-token balance in the ledger account. Your organization is billed at the end of the month for accrued gas fees. ::: Example `step.confirmed` webhook for a stablecoin payout after blockchain network confirmation: ```json { "id": "d7e80c94-d7b3-4e9b-8c00-065e7cbcfac8", "type": "step.confirmed", "created_at": "2025-12-01T09:00:01.802Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": "0xd65dc6bf6dcc111237f9acfbfa6003ea4a4d88f2e071f4307d3af81ae877f7be", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "circle_mint", "status": "confirmed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:01.800Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": null, "failed_at": null } } } ``` Note: at `step.confirmed`, the per-step `actual.*` is still all-null. Step-level `actual.{from,to}` becomes populated only at `step.completed` to minimize the risk of reporting values that subsequently change. Example `step.confirmed` webhook for the on-chain step of a fiat payout (Circle ledger → Alfred USDC ledger): ```json { "id": "d7e80c94-d7b3-4e9b-8c00-065e7cbcfac8", "type": "step.confirmed", "created_at": "2025-12-01T09:00:01.802Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": "0x2b3c4d5e6f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "circle_mint", "status": "confirmed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:01.800Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": null, "failed_at": null } } } ``` ## 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 (`provider_key: "alfred"`) and progresses through the same `created → submitted → confirmed → completed` path. See [Step Statuses](/overviews/funds-movement-lifecycle-and-data-model#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](/overviews/funds-movement-lifecycle-and-data-model#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 **second** `payment.updated` event you receive — the first fired after the PATCH supplied account ids (see [Update payout with account information](#update-payout-with-account-information)). Example terminal-state `payment.updated` webhook for a successful stablecoin payout: ```json { "id": "f4c8e1a3-9b76-4d2e-a058-3c5f9e1d4b27", "type": "payment.updated", "created_at": "2025-12-01T09:00:02.502Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "transaction_hash": "0xd65dc6bf6dcc111237f9acfbfa6003ea4a4d88f2e071f4307d3af81ae877f7be", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:02.500Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": "2025-12-01T09:00:02.500Z", "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:02.500Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example terminal-state `payment.updated` webhook for a successful fiat payout: ```json { "id": "f4c8e1a3-9b76-4d2e-a058-3c5f9e1d4b27", "type": "payment.updated", "created_at": "2025-12-01T09:00:02.502Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "transaction_hash": "0x2b3c4d5e6f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "circle_mint", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:02.000Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": "2025-12-01T09:00:02.000Z", "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "transaction_hash": null, "fees": [ { "fee_amount": "1.50", "fee_currency": "USDC", "fee_type": "provider", "fee_metadata": {} } ], "provider_key": "alfred", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:02.500Z", "submitted_at": "2025-12-01T09:00:02.100Z", "confirmed_at": "2025-12-01T09:00:02.300Z", "completed_at": "2025-12-01T09:00:02.500Z", "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:02.500Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ## Failure Modes for Custodian Payouts This section covers the common non-happy-path states a custodian payout may enter. For the full status taxonomy applicable to all funds-movement resources, see [Statuses Reference](/overviews/funds-movement-lifecycle-and-data-model#statuses-reference). ### Awaiting funds If the Circle ledger at `desired.from.account_id` does not have sufficient funds when Tesser performs the balance check, the payout enters `balance_status: "awaiting_funds"` and execution halts until either funds arrive or the payout expires. What you will observe: - A `payment.balance_updated` webhook fires with `balance_status: "awaiting_funds"` and `balance_reserved_at: null`. - No steps are submitted; the payout sits idle. - When sufficient funds arrive at the Circle ledger, Tesser publishes a second `payment.balance_updated` webhook with `balance_status: "reserved"` and `balance_reserved_at` populated. **Execution then resumes automatically — no client action is required.** - If the payout `expires_at` passes before funds arrive, see [Expiration before completion](#expiration-before-completion). Example `payment.balance_updated` webhook with `balance_status: "awaiting_funds"`: ```json { "id": "5c1f3e9b-4a6d-47c2-9f08-8d2c7e1b5a04", "type": "payment.balance_updated", "created_at": "2025-12-01T09:00:01.002Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "awaiting_funds", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:01.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ### Risk rejection If the risk policy auto-rejects the payout (or an operator manually rejects during review), execution halts before the balance check. What you will observe: - A `payment.risk_updated` webhook fires with `risk_status: "auto_rejected"` or `risk_status: "manually_rejected"`. - `risk_status_reasons` carries the type, category, and severity that drove the decision. - No `payment.balance_updated` event fires; no steps are submitted. - The payout is terminal in this state. To proceed, resolve the underlying risk factors and create a new payout. Example `payment.risk_updated` webhook with `risk_status: "auto_rejected"`: ```json { "id": "9d3a7e21-b4c8-4f15-a02d-6e8b4c1f9a72", "type": "payment.risk_updated", "created_at": "2025-12-01T09:00:00.602Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_rejected", "risk_status_reasons": [ { "risk_status_reason_type": "indirect", "risk_status_reason_category": "malware", "risk_status_reason_severity": "severe" } ], "risk_reviewed_by": null, "risk_reviewed_at": "2025-12-01T09:00:00.600Z", "balance_status": "unreserved", "balance_reserved_at": null, "steps": [], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.600Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ### Step failure A step may fail during execution — for example, an on-chain transaction reverts, the off-ramp provider rejects the transfer, or the upstream custodian returns an error. What you will observe: - A `step.failed` webhook fires for the affected step with `status: "failed"` and a populated `status_reasons` array. - The parent payment's terminal `payment.updated` webhook fires shortly after, with the failed step reflected in `steps[]`. - Top-level `actual.*` may diverge from `desired.*` / `estimated.*` — see [Terminal State and Divergence](/overviews/funds-movement-lifecycle-and-data-model#terminal-state-and-divergence). - Subsequent steps in the route do not execute. Example `step.failed` webhook for the on-chain step of a custodian payout: ```json { "id": "ab7c2e91-3d4f-46b8-9c05-1f2e8a4b7c93", "type": "step.failed", "created_at": "2025-12-01T09:00:01.802Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "circle_mint", "status": "failed", "status_reasons": [ { "error_code": "payment-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:01.800Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T09:00:01.800Z" } } } ``` ### Expiration before completion Every payout has an `expires_at` timestamp. If the payout has not reached terminal state by then, it cannot be resumed — to retry, create a new payout. See [Expiration](/overviews/funds-movement-lifecycle-and-data-model#expiration) for the full semantics. What you will observe: - Common precondition states that lead here: `balance_status` stuck in `awaiting_funds`, manual risk decision not submitted in time, or a step that retried-and-recovered too slowly. - The payout's terminal `payment.updated` webhook reflects the partial state at the moment of expiry — top-level `actual.*` is partially populated (or fully null if no step ever submitted), and any non-terminal steps remain in their pre-execution status. - No further state transitions occur after expiry. Example terminal `payment.updated` webhook for a payout that expired while `awaiting_funds`: ```json { "id": "e8a4b3c1-7d92-4f06-a1e5-3b8c2f1d4e07", "type": "payment.updated", "created_at": "2025-12-01T23:59:59.999Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "awaiting_funds", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "circle_mint", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:01.000Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T23:59:59.999Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` --- ## Document: Create a Payout (from a Wallet) Create and execute payouts using self-custodial wallets URL: /how-tos/send-a-stablecoin-payout/create-a-payout-from-a-wallet # Create a Payout (from a Wallet) 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](/overviews/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. ## Payout Creation To create a payout, send a POST request to the [Payments API](/api/payments#create-payment). 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](#update-payout-with-account-information). Example request for stablecoin payout creation: ```json { "desired": { "from": { "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "currency": "USDC", "network": "ETHEREUM" } }, "organization_reference_id": "ref_123" } ``` Example request for fiat payout creation: ```json { "desired": { "from": { "currency": "USDC", "network": "ETHEREUM" }, "to": { "amount": "1000", "currency": "MXN", "network": null } }, "organization_reference_id": "ref_123" } ``` :::note 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. Example response for stablecoin payout creation: ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": null, "desired": { "from": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } ``` Example response for fiat payout creation: ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": null, "desired": { "from": { "account_id": null, "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } ``` ## Quote and Planning 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](/overviews/funds-movement-lifecycle-and-data-model#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. :::note 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 updates the plan (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): ```json { "id": "8472fb87-73b3-45ee-8020-a3496b4fc7a1", "type": "payment.quote_created", "created_at": "2025-12-01T09:00:00.045Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": null, "desired": { "from": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.040Z", "signature_requested_at": null, "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.040Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example `payment.quote_created` webhook for a fiat payout (cross-currency, two steps): ```json { "id": "8472fb87-73b3-45ee-8020-a3496b4fc7a1", "type": "payment.quote_created", "created_at": "2025-12-01T09:00:00.045Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": null, "desired": { "from": { "account_id": null, "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": null, "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": null, "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": null, "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.040Z", "signature_requested_at": null, "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": null, "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "provider", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.040Z", "signature_requested_at": null, "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.040Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ## Update Payout with Account Information If you did not supply account information at creation, submit a PATCH request to the [Payments API](/api/payments#update-payment) 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. :::note 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](/overviews/compliance-and-risk#travel-rule). ::: Example PATCH request for stablecoin payout updated with accounts: ```json { "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012" } } } ``` Example PATCH request for fiat payout updated with accounts: ```json { "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047" } } } ``` 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](#payout-terminal-state).) Tesser will also update the route plan 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 updated `steps[]`. Example `payment.updated` webhook after PATCHing accounts on a stablecoin payout: ```json { "id": "1c9a4f6e-8b3d-4f23-a056-9d7c4b2e1f08", "type": "payment.updated", "created_at": "2025-12-01T09:00:00.205Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "signature_requested_at": null, "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.200Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example `payment.updated` webhook after PATCHing accounts on a fiat payout: ```json { "id": "1c9a4f6e-8b3d-4f23-a056-9d7c4b2e1f08", "type": "payment.updated", "created_at": "2025-12-01T09:00:00.205Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "unchecked", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "unreserved", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "signature_requested_at": null, "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "provider", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "signature_requested_at": null, "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.200Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ## Payout Risk Review 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](/overviews/funds-movement-lifecycle-and-data-model#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](/api/payments#submit-risk-review) 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. :::warning{title="Payouts expire"} Ensure you proceed with step signing prior to the expiration time, as indicated by the payout's `expires_at` timestamp. If a payout expires, it cannot be resurrected. To retry, create a new payout. See [Expiration](/overviews/funds-movement-lifecycle-and-data-model#expiration) for details. ::: Example webhook schema for stablecoin payout requesting signature: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "step.signature_requested", "created_at": "2025-12-01T09:00:00.802Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "signature_requested", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:00.800Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } } } ``` Example webhook schema for fiat payout requesting signature: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "step.signature_requested", "created_at": "2025-12-01T09:00:00.802Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "signature_requested", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:00.800Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } } } ``` 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](/api/payments#sign-payment-step) 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. ```typescript import { LocalSigner, TesserApi } from "@tesser-payments/sdk"; // Initialize API client const 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 event const step = webhookPayload.data.object; // 2. Sign the step const result = await signer.signStep(step); // 3. Submit the signature to execute the step await 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 }), } ); ``` The `signStep` method returns: - `signature`: send this to the [Sign payment step API](/api/payments#sign-payment-step) to execute the payment. - `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](/overviews/funds-movement-lifecycle-and-data-model#balance-statuses) on the lifecycle overview. If there are sufficient funds in the wallet, you receive a success response from the [Sign payment step API](/api/payments#sign-payment-step), 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; step `status` becomes `signed`. Example webhook schema for stablecoin payout after successful balance check: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "payment.balance_updated", "created_at": "2025-12-01T09:00:01.002Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "signature_requested", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:00.800Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:01.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example webhook schema for fiat payout after successful balance check: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "payment.balance_updated", "created_at": "2025-12-01T09:00:01.002Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "signature_requested", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:00.800Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "provider", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "signature_requested_at": null, "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:01.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example webhook schema for stablecoin payout after signing: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "step.signed", "created_at": "2025-12-01T09:00:01.052Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "signed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:01.050Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } } } ``` Example webhook schema for fiat payout after signing: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "step.signed", "created_at": "2025-12-01T09:00:01.052Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "signed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:01.050Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } } } ``` 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: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "payment.balance_updated", "created_at": "2025-12-01T09:00:01.002Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "awaiting_funds", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "signature_requested", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:00.800Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:01.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example webhook schema for fiat payout after failed balance check: ```json { "id": "99ea4da4-2178-4169-9e21-01d3cb4ae158", "type": "payment.balance_updated", "created_at": "2025-12-01T09:00:01.002Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "awaiting_funds", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "signature_requested", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:00.800Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "provider", "status": "created", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:00.200Z", "signature_requested_at": null, "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:01.000Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ## 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](/overviews/funds-movement-lifecycle-and-data-model#execution)). The blockchain `transaction_hash` is populated on the step once submitted, and gas fees appear in the per-step `fees[]` array once confirmed. :::note 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: ```json { "id": "d7e80c94-d7b3-4e9b-8c00-065e7cbcfac8", "type": "step.confirmed", "created_at": "2025-12-01T09:00:01.802Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": "0xd65dc6bf6dcc111237f9acfbfa6003ea4a4d88f2e071f4307d3af81ae877f7be", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "turnkey", "status": "confirmed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:01.800Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": null, "failed_at": null } } } ``` Note: at `step.confirmed`, the per-step `actual.*` is still all-null. Step-level `actual.{from,to}` becomes populated at `step.completed` — the design defers population to the terminal step state to minimize the risk of reporting values that subsequently change. Example webhook schema for fiat payout after blockchain network confirmation: ```json { "id": "d7e80c94-d7b3-4e9b-8c00-065e7cbcfac8", "type": "step.confirmed", "created_at": "2025-12-01T09:00:01.802Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": "0x2b3c4d5e6f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "turnkey", "status": "confirmed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:01.800Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": null, "failed_at": null } } } ``` ## 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](/overviews/funds-movement-lifecycle-and-data-model#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](/overviews/funds-movement-lifecycle-and-data-model#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 **second** `payment.updated` event you receive — the first fired after the PATCH supplied account ids (see [Update payout with account information](#update-payout-with-account-information)). Example terminal-state `payment.updated` webhook for a successful stablecoin payout: ```json { "id": "f4c8e1a3-9b76-4d2e-a058-3c5f9e1d4b27", "type": "payment.updated", "created_at": "2025-12-01T09:00:02.502Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "transaction_hash": "0xd65dc6bf6dcc111237f9acfbfa6003ea4a4d88f2e071f4307d3af81ae877f7be", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "turnkey", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:02.500Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": "2025-12-01T09:00:02.500Z", "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:02.500Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` Example terminal-state `payment.updated` webhook for a successful fiat payout: ```json { "id": "f4c8e1a3-9b76-4d2e-a058-3c5f9e1d4b27", "type": "payment.updated", "created_at": "2025-12-01T09:00:02.502Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "transaction_hash": "0x2b3c4d5e6f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "turnkey", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:02.000Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": "2025-12-01T09:00:02.000Z", "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "transaction_hash": null, "fees": [ { "fee_amount": "1.50", "fee_currency": "USDC", "fee_type": "provider", "fee_metadata": {} } ], "provider_key": "provider", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:02.500Z", "signature_requested_at": null, "signed_at": null, "submitted_at": "2025-12-01T09:00:02.100Z", "confirmed_at": "2025-12-01T09:00:02.300Z", "completed_at": "2025-12-01T09:00:02.500Z", "failed_at": null } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:02.500Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ## Webhook Events by Scenario Below are the webhook events you will observe for each of the two wallet payout scenarios in this guide. Both scenarios show the two-step creation flow (POST without account_ids, then PATCH to fill them) for completeness; if you supply all account_ids at creation, the PATCH row is omitted and `payment.quote_created` fires once with the full `desired.*` overlay populated. **Scenario 1 — Crypto destination (wallet → external crypto address)** | # | What happens | What you receive / do | Fields populated | |---|---|---|---| | 1 | You submit the payout request (you may supply all account_ids at creation, or only the minimum and supply the rest via PATCH) | HTTP response with `id`; `desired.*` populated with what you supplied | `desired.from.amount` or `desired.to.amount` (whichever you supplied); any account_ids you supplied | | 2 | Tesser plans the route and obtains a quote | `payment.quote_created` webhook fires with the populated `estimated` overlay and a single on-chain transfer step. If account_ids were not supplied at creation, the quote fires with null account_ids in `estimated.*`/`desired.*` (updated route plan happens after PATCH). | Steps array; `estimated.from.amount` and `estimated.to.amount` at payout and step level | | 3 | _(only in the two-step flow)_ You PATCH the payout with `funding_account_id`, `desired.from.account_id`, and `desired.to.account_id` | `payment.updated` webhook fires with the now-complete `desired.*` overlay and updated `steps[]` | Top-level `desired.from.account_id`, `desired.to.account_id`, `funding_account_id`; updated `estimated.*` on steps | | 4 | Tesser performs risk review on the destination | `payment.risk_updated` webhook fires with the risk outcome | `risk_status`; `risk_status_reasons` if `rejected` or `manual_review` | | 5 | Tesser asks you to sign the on-chain transfer | `step.signature_requested` on the step | Step 1: `status` updates; signing payload supplied via the API | | 6 | You submit the signature | `POST /v1/payments/{paymentId}/steps/{stepId}/sign` with `{ "signature": "0x..." }`. Tesser checks the wallet balance and emits `payment.balance_updated` with `balance_status: "reserved"` once the signed step is accepted, then emits `step.signed` for the step. | `balance_status`, `balance_reserved_at`; step `status: "signed"` | | 7 | Tesser broadcasts the signed transaction | `step.submitted` on the step | Step 1: `submitted_at`, `transaction_hash` | | 8 | The on-chain transaction is visible on the network | `step.confirmed` on the step | Step 1: `confirmed_at` | | 9 | The transfer reaches finality; payout is complete | `step.completed` on the step, followed by `payment.updated` with the terminal Payment object | Step 1: `completed_at`, `actual.from.amount`, `actual.to.amount`. Payout-level `actual.from.amount` and `actual.to.amount`. | **Scenario 2 — Fiat destination (wallet → fiat provider → external bank)** | # | What happens | What you receive / do | Fields populated | |---|---|---|---| | 1 | You submit the payout request (you may supply all account_ids at creation, or only the minimum and supply the rest via PATCH) | HTTP response with `id`; `desired.*` populated with what you supplied | `desired.from.amount` or `desired.to.amount` (whichever you supplied); any account_ids you supplied | | 2 | Tesser plans the route and obtains a quote | `payment.quote_created` webhook fires with two steps (on-chain transfer to the fiat provider, then fiat transfer from the fiat provider) and the populated `estimated` overlay. If account_ids were not supplied at creation, the quote fires with null account_ids in `estimated.*`/`desired.*` (route plan updating happens after PATCH). | Steps array; `estimated.*` at payout and step level | | 3 | _(only in the two-step flow)_ You PATCH the payout with `funding_account_id`, `desired.from.account_id`, and `desired.to.account_id` | `payment.updated` webhook fires with the now-complete `desired.*` overlay and updated `steps[]` | Top-level `desired.from.account_id`, `desired.to.account_id`, `funding_account_id`; updated `estimated.*` on steps | | 4 | Tesser performs risk review on the destination | `payment.risk_updated` webhook fires with the risk outcome | `risk_status`; `risk_status_reasons` if `rejected` or `manual_review` | | 5 | Tesser asks you to sign the on-chain transfer to the fiat provider | `step.signature_requested` on Step 1 | Step 1: `status` updates; signing payload supplied via the API | | 6 | You submit the signature | `POST /v1/payments/{paymentId}/steps/{stepId}/sign` with `{ "signature": "0x..." }`. Tesser checks the wallet balance and emits `payment.balance_updated` with `balance_status: "reserved"` once the signed step is accepted, then emits `step.signed` for Step 1. | `balance_status`, `balance_reserved_at`; Step 1 `status: "signed"` | | 7 | Tesser broadcasts the signed transaction | `step.submitted` on Step 1 | Step 1: `submitted_at`, `transaction_hash` | | 8 | The on-chain transaction is visible on the network | `step.confirmed` on Step 1 | Step 1: `confirmed_at` | | 9 | The on-chain transfer to the fiat provider reaches finality | `step.completed` on Step 1 | Step 1: `completed_at`, `actual.from.amount`, `actual.to.amount` | | 10 | The fiat provider initiates the fiat wire to the external bank | `step.submitted` on Step 2 | Step 2: `submitted_at` | | 11 | The fiat provider confirms the transfer | `step.confirmed` on Step 2 | Step 2: `confirmed_at` | | 12 | Funds settle at the external bank; payout is complete | `step.completed` on Step 2, followed by `payment.updated` with the terminal Payment object | Step 2: `completed_at`, `actual.from.amount`, `actual.to.amount`. Payout-level `actual.from.amount` and `actual.to.amount`. | ## Failure Modes for Wallet Payouts This section covers the common non-happy-path states a wallet payout may enter. For the full status taxonomy applicable to all funds-movement resources, see [Statuses Reference](/overviews/funds-movement-lifecycle-and-data-model#statuses-reference). ### Awaiting funds If the wallet has insufficient funds when you submit the signature, the payout enters `balance_status: "awaiting_funds"` and waits until the wallet is funded or the payout expires. See [Balance Check](#balance-check) above for the full description and webhook example payloads. ### Risk rejection If risk review at `payment.risk_updated` returns `risk_status: "rejected"`, the payout transitions to a terminal failed state before any signing is requested. No funds move; planned steps go directly from `created` to `failed`. What you will observe: - A `payment.risk_updated` webhook fires with `risk_status: "rejected"` and `risk_status_reasons` populated with the reason codes. - A terminal `payment.updated` webhook follows. Top-level `actual.*` is all null (no movement occurred). Each step in `steps[]` transitions to `status: "failed"` with `actual.*` all null and `status_reasons` populated. - The payout is terminal in this state. To proceed, resolve the underlying risk factors and create a new payout. Example terminal `payment.updated` webhook for a risk-rejected stablecoin payout: ```json { "id": "a8b3d2e7-4c91-4f5a-9d28-1e7c3f8a5b04", "type": "payment.updated", "created_at": "2025-12-01T09:00:00.500Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "rejected", "risk_status_reasons": [ { "error_code": "payments-XXXX", "error_message": "placeholder error message" } ], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": null, "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "failed", "status_reasons": [ { "error_code": "payments-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:00.500Z", "signature_requested_at": null, "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T09:00:00.500Z" } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:00.500Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ### Step failure A signed step can fail after broadcast, or a downstream provider step can fail after a customer-signed step completes. **Scenario 1 — On-chain broadcast failure** If the on-chain transfer step is signed and broadcast but fails to confirm (chain reorg, gas exhaustion, or other broadcast issue), the step transitions to `failed`. Per the failed-step rule, the step's `actual.*` is all null and `status_reasons` carries the failure detail. Top-level `actual.*` is all null because no step reached `completed` status (broadcast does not count as completion). What you will observe: - A `step.failed` webhook fires for the on-chain step. Step-level `actual.*` is null; `status_reasons` carries the failure detail. `submitted_at` and `transaction_hash` are populated (the broadcast attempt yielded a hash before the on-chain failure); `confirmed_at` and `completed_at` are null; `failed_at` is populated. - A terminal `payment.updated` webhook follows. Top-level `actual.*` is all null because no step completed. Example `step.failed` webhook for an on-chain broadcast failure: ```json { "id": "c2f8e1a4-7b5d-4936-a0c8-3e9f1d2b4a7c", "type": "step.failed", "created_at": "2025-12-01T09:00:01.900Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": "0x8a4f1e6c9b2d7a3f5e8c1b4d9f6a2e7c5b8d1f4e7a0c3b6d9f2e5a8c1b4d7f0e", "fees": [], "provider_key": "turnkey", "status": "failed", "status_reasons": [ { "error_code": "payments-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:01.900Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T09:00:01.900Z" } } } ``` Example terminal `payment.updated` webhook for an on-chain broadcast failure: ```json { "id": "d4a9e3c1-6b82-4715-9e0d-2f5c8a1b3e64", "type": "payment.updated", "created_at": "2025-12-01T09:00:01.902Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": "0x8a4f1e6c9b2d7a3f5e8c1b4d9f6a2e7c5b8d1f4e7a0c3b6d9f2e5a8c1b4d7f0e", "fees": [], "provider_key": "turnkey", "status": "failed", "status_reasons": [ { "error_code": "payments-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:01.900Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T09:00:01.900Z" } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:01.902Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` **Scenario 2 — Fiat-side failure** If the on-chain transfer to the fiat provider succeeds but the subsequent fiat wire fails, Step 1 is `completed` and Step 2 is `failed`. The payout terminates with funds stranded at the fiat provider's holding ledger. Per the failed-step rule, Step 2's `actual.*` is all null. Per the first/last-completed rule, the top-level `actual.from` matches Step 1's `actual.from` (the wallet debit) and the top-level `actual.to` matches Step 1's `actual.to` (USDC at the fiat provider's holding ledger). What you will observe: - Step 1 (`step.completed`) fires normally with both `actual.*` overlays populated. The customer's wallet has been debited; the fiat provider's holding ledger has received the USDC. - A `step.failed` webhook fires for Step 2. `actual.*` is all null; `status_reasons` carries the failure detail (e.g., bank rejection, OFAC issue). - A terminal `payment.updated` webhook follows. Top-level `actual.from` matches Step 1's `actual.from` (USDC at the source wallet); top-level `actual.to` matches Step 1's `actual.to` (USDC at the fiat provider's holding ledger). Funds are stranded at the fiat provider until manual recovery. Example terminal `payment.updated` webhook for a fiat-side failure: ```json { "id": "e9d4a7b2-3c6f-4815-b9e2-5d8c1f3a7b04", "type": "payment.updated", "created_at": "2025-12-01T09:00:03.100Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": null, "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "reserved", "balance_reserved_at": "2025-12-01T09:00:01.000Z", "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "amount": "55.85", "currency": "USDC", "network": "ETHEREUM" } }, "transaction_hash": "0xd65dc6bf6dcc111237f9acfbfa6003ea4a4d88f2e071f4307d3af81ae877f7be", "fees": [ { "fee_amount": "0.01", "fee_currency": "USDC", "fee_type": "gas", "fee_metadata": {} } ], "provider_key": "turnkey", "status": "completed", "status_reasons": [], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T09:00:02.500Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": "2025-12-01T09:00:01.050Z", "submitted_at": "2025-12-01T09:00:01.200Z", "confirmed_at": "2025-12-01T09:00:01.800Z", "completed_at": "2025-12-01T09:00:02.500Z", "failed_at": null }, { "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 2, "step_type": "transfer", "estimated": { "from": { "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b", "amount": "55.85", "currency": "USDC", "network": null }, "to": { "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047", "amount": "1000", "currency": "MXN", "network": null } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "provider", "status": "failed", "status_reasons": [ { "error_code": "payments-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T09:00:00.040Z", "updated_at": "2025-12-01T09:00:03.100Z", "signature_requested_at": null, "signed_at": null, "submitted_at": "2025-12-01T09:00:02.700Z", "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T09:00:03.100Z" } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T09:00:03.100Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ``` ### Expiration before completion Every payout has an `expires_at` timestamp. If the payout has not reached terminal state by then, it cannot be resumed — to retry, create a new payout. See [Expiration](/overviews/funds-movement-lifecycle-and-data-model#expiration) for the full semantics. What you will observe: - A `step.failed` webhook fires for each step that didn't reach terminal state. Each failed step's `actual.*` is all null; `status_reasons` carries the expiration detail. - A `payment.updated` webhook fires reporting the final state. Top-level `actual.*` is all null because no movement occurred. `balance_status` is preserved as `"awaiting_funds"`. Example terminal `payment.updated` webhook for a payout that expired while `awaiting_funds`: ```json { "id": "b7e3c1f2-9d48-4a56-8c0f-2e9d4a7b1f30", "type": "payment.updated", "created_at": "2025-12-01T23:59:59.999Z", "data": { "object": { "id": "550e8400-e29b-41d4-a716-446655440020", "workspace_id": "550e8400-e29b-41d4-a716-446655440001", "organization_reference_id": "ref_123", "direction": "outbound", "funding_account_id": "550e8400-e29b-41d4-a716-446655440010", "desired": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": null, "currency": "USDC", "network": "ETHEREUM" } }, "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "risk_status": "auto_approved", "risk_status_reasons": [], "risk_reviewed_by": null, "risk_reviewed_at": null, "balance_status": "awaiting_funds", "balance_reserved_at": null, "steps": [ { "id": "550e8400-e29b-41d4-a716-446655440100", "transfer_id": "550e8400-e29b-41d4-a716-446655440020", "step_sequence": 1, "step_type": "transfer", "estimated": { "from": { "account_id": "550e8400-e29b-41d4-a716-446655440011", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" }, "to": { "account_id": "550e8400-e29b-41d4-a716-446655440012", "amount": "1000", "currency": "USDC", "network": "ETHEREUM" } }, "actual": { "from": { "account_id": null, "amount": null, "currency": null, "network": null }, "to": { "account_id": null, "amount": null, "currency": null, "network": null } }, "transaction_hash": null, "fees": [], "provider_key": "turnkey", "status": "failed", "status_reasons": [ { "error_code": "payments-XXXX", "error_message": "placeholder error message" } ], "created_at": "2025-12-01T09:00:00.400Z", "updated_at": "2025-12-01T23:59:59.999Z", "signature_requested_at": "2025-12-01T09:00:00.800Z", "signed_at": null, "submitted_at": null, "confirmed_at": null, "completed_at": null, "failed_at": "2025-12-01T23:59:59.999Z" } ], "created_at": "2025-12-01T09:00:00.000Z", "updated_at": "2025-12-01T23:59:59.999Z", "expires_at": "2025-12-01T23:59:59.999Z" } } } ```