# 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/manage-your-treasury/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 asynchronously 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. Ledger-to-wallet rebalances swap first and then transfer the resulting stablecoin. Wallet-to-OpenFX rebalances transfer the stablecoin to the OpenFX ledger first and then swap it there.

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 | 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.*

### Kraken Ledger Swaps

To convert BRL held at Kraken to USDC, use the same managed Kraken account ID on both sides and omit both networks. This route is available in production.

```json
{
  "desired": {
    "from": {
      "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
      "amount": "1000",
      "currency": "BRL"
    },
    "to": {
      "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
      "currency": "USDC"
    }
  }
}
```

Tesser plans two market orders: BRL1→USD, followed by USD→USDC. BRL1 is Kraken's asset underlying the BRL balance in Tesser. Both planned order quantities remain fixed during execution. The actual USD proceeds of the first step stay reserved for the second step, whose actual debit can differ from those proceeds. Read each step's `actual` and `fees` fields for settled amounts; `estimated` remains the original quote.

CAD→USDC and USD→USDC use the same request shape with the corresponding source currency and require one swap step. USD→USDC can also convert USD left available by a failed BRL route. For partial execution and uncertain submissions, see [Kraken rebalance behavior](/overviews/deposits-withdrawals-and-rebalances#rebalances).

To deliver the resulting USDC to your managed wallet, create a separate USDC→USDC rebalance from the Kraken ledger to the wallet with `desired.to.network = "BASE"`. The wallet address must be registered and verified in Kraken. Tesser accounts for Kraken's withdrawal fee in the amount delivered.

### Wallet to Kraken Ledger

This route moves USDC from your managed wallet to your managed Kraken ledger in the same workspace. Tempo uses USDC.e; Ethereum, Polygon, and Base use the configured native USDC contract. Deposit networks remain unavailable until Tesser completes acceptance and enables them. Base is disabled by default.

1. Submit `POST /v1/treasury/rebalances` with the wallet network on `desired.from` and no network on `desired.to`. Tesser selects the matching Kraken method and reuses or provisions a receiving address.
   - An unavailable network, unsupported currency, invalid amount, or provider minimum prevents the transfer from being sent. Address provisioning requires no customer-operated Kraken request.
2. Wait for a step in `signature_requested`, then sign its `unsigned_transaction` and submit the usual Turnkey envelope to `POST /v1/treasury/rebalances/{id}/steps/{stepId}/sign`.
   - The prepared destination and amount stay fixed. If they are no longer valid before submission, the step fails and a new rebalance requires a new signature.
3. Observe the saved rebalance through GET or treasury webhooks. Tesser completes it after verifying both chain delivery and Kraken's actual credit.
   - Chain confirmation can precede Kraken credit. Keep observing the same operation instead of creating a replacement transfer.
   - `estimated.to.amount` is the projected net credit. `actual.from.amount` is the wallet principal, `actual.to.amount` is the verified net Kraken amount, and `fees` reports provider and chain fees. For example, sending `5` USDC with a verified `0.01` USDC deposit fee credits `4.99` USDC.
   - Missing or conflicting transaction evidence remains pending for reconciliation. Expiration after submission does not make sent principal available again.

Example request for an enabled Tempo deposit route:

```json
{
  "organization_reference_id": "wallet-to-kraken-example",
  "desired": {
    "from": {
      "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
      "amount": "5",
      "currency": "USDC",
      "network": "TEMPO"
    },
    "to": {
      "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
      "currency": "USDC"
    }
  }
}
```

Amounts in examples are illustrative; Kraken minimums and fees are checked for each operation. Fiat conversion remains a separate Kraken rebalance.

### Wallet to OpenFX Ledger

OpenFX also supports rebalancing a stablecoin from a self-custodial wallet into your OpenFX ledger. Set `desired.from.network` to the wallet's network and omit `desired.to.network` because the destination is a ledger. When the source and destination currencies match, the route contains one signed on-chain transfer. When they differ, Tesser transfers the source stablecoin first and then swaps it at OpenFX.

Example request (rebalance USDC from a self-custodial wallet on BASE into USD at an OpenFX ledger):

```json
{
  "tenant_id": null,
  "desired": {
    "from": {
      "account_id": "7f3e9c2a-8d5b-4a1e-9f6c-3b8e1d4a7c5f",
      "amount": "1000",
      "currency": "USDC",
      "network": "BASE"
    },
    "to": {
      "account_id": "2e8f4c6b-3a1d-48e7-9b5c-0f4d2c9a7e3b",
      "currency": "USD"
    }
  }
}
```

:::note{title="Testing this flow in sandbox"}
OpenFX does not provide on-chain deposit addresses in its sandbox. In sandbox and staging, Tesser sends the signed transfer to a Tesser-controlled sandbox address instead. After the transfer step reaches `submitted`, create a deposit in the OpenFX sandbox account connected to Tesser with the same amount and currency as the transfer step and the corresponding network. Tesser completes the transfer step and starts any following swap after OpenFX sends the completed deposit webhook.

Create the OpenFX sandbox deposit only after the transfer step is submitted, and run only one pending wallet-to-OpenFX flow with the same amount, currency, and network at a time. Identical pending flows cannot be matched safely. Production sends funds directly to the registered OpenFX deposit address and does not require this manual deposit.

If the OpenFX deposit does not match a submitted flow, Tesser does not advance the rebalance. Instead, Tesser records a completed inbound Payment to your OpenFX ledger, emits `payment.created`, and adds the received amount to the ledger's `available_balance`. If the deposit could match more than one submitted flow, Tesser does not choose between them: no flow advances, and Tesser does not create a Payment or update the ledger's tracked balance. Contact Tesser support before creating another deposit or retrying the rebalance. See [OpenFX Ledger Inbound Payments](/webhooks/payment-updates#openfx-ledger-inbound-payments) for the inbound Payment's event behavior.
:::

**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)

<Tabs>
  <TabItem label="Scenario 1">

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"
    }
  }
}
```

  </TabItem>
  <TabItem label="Scenario 2">

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"
    }
  }
}
```

  </TabItem>
  <TabItem label="Scenario 3">

Example request (rebalance USDT on POLYGON between two self-custodial wallets):

```json
{
  "tenant_id": null,
  "desired": {
    "from": {
      "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
      "amount": "1000",
      "currency": "USDT",
      "network": "POLYGON"
    },
    "to": {
      "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f",
      "currency": "USDT",
      "network": "POLYGON"
    }
  }
}
```

  </TabItem>
</Tabs>

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.

<Tabs>
  <TabItem label="Scenario 1">

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"
  }
}
```

  </TabItem>
  <TabItem label="Scenario 2">

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"
  }
}
```

  </TabItem>
  <TabItem label="Scenario 3">

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": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
        "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"
  }
}
```

  </TabItem>
</Tabs>

## 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.

<Tabs>
  <TabItem label="Scenario 1">

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",
          "rebalance_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"
    }
  }
}
```

  </TabItem>
  <TabItem label="Scenario 2">

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",
          "rebalance_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",
          "rebalance_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"
    }
  }
}
```

  </TabItem>
  <TabItem label="Scenario 3">

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": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
          "amount": "1000",
          "currency": "USDT",
          "network": "POLYGON"
        },
        "to": {
          "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f",
          "amount": null,
          "currency": "USDT",
          "network": "POLYGON"
        }
      },
      "estimated": {
        "from": {
          "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
          "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",
          "rebalance_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd",
          "step_sequence": 1,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
              "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"
    }
  }
}
```

  </TabItem>
</Tabs>

## Rebalance Balance Check (`rebalance.balance_updated`)

Rebalances debit funds from the managed account at `desired.from.account_id`, so Tesser performs a balance check before the rebalance executes. When the balance check runs and how it surfaces depends on the type of account:

- **Ledger sources (Scenarios 1 and 2):** the check runs asynchronously after the rebalance is created. Shortly after `rebalance.quote_created` fires, Tesser reads the ledger's available balance and emits `rebalance.balance_updated` with the outcome.
- **Wallet sources (Scenario 3 and wallet-to-OpenFX rebalances):** the check runs synchronously inside your call to the [step-signing API](/api/treasury#sign-rebalance-step). On success, the sign endpoint accepts your signature and `rebalance.balance_updated` fires with `balance_status: "reserved"` from the sign-API path alongside `step.signed`. On insufficient funds, the sign endpoint returns a 4XX response and `rebalance.balance_updated` fires with `balance_status: "awaiting_funds"`. Tesser monitors the wallet's on-chain balance and republishes `step.signature_requested` (with a refreshed `signature_requested_at`) once the wallet is funded; repeat the signing flow with the refreshed step to retry, until `expires_at`.

In both cases, a `reserved` `balance_status` means execution can proceed and `balance_reserved_at` records when the reservation succeeded. See [Balance Statuses](/overviews/funds-movement-lifecycle-and-data-model#balance-statuses).

:::note
If `desired.from.account_id` does not have sufficient funds when the balance check runs, `balance_status` is set to `awaiting_funds`. For ledger sources, Tesser republishes `rebalance.balance_updated` as the ledger balance is re-evaluated. For wallet sources, Tesser republishes `step.signature_requested` once on-chain funding is observed, and the next `rebalance.balance_updated` (with `balance_status: "reserved"`) fires on the subsequent successful sign. The retry loop continues until funds arrive or the rebalance expires — see [Failure Modes for Rebalances](#failure-modes-for-rebalances).
:::

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",
          "rebalance_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

Tesser initiates the internal move via Circle's API and observes Circle's synchronous confirmation, so `step.submitted`, `step.confirmed`, and `step.completed` arrive in close succession. 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",
      "rebalance_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": "2025-12-01T10:05:00.300Z",
      "confirmed_at": "2025-12-01T10:05:00.400Z",
      "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",
      "rebalance_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": "0x7e8c3f2a1b4d ... 7c8d9e0f",
      "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, producing `step.signed`. Tesser then broadcasts on-chain, producing `step.submitted`, `step.confirmed` (with `transaction_hash`), and finally `step.completed`.

If the same wallet has an earlier transfer still waiting to be broadcast, this one queues behind it: the sign call returns 200 and the step stays `signed`, already carrying its `transaction_hash` (derived from the signed payload) with `submitted_at` still `null`. It broadcasts as soon as the earlier transfers are submitted, and `step.submitted` / `step.confirmed` follow as above — no action needed on your side. See [signing a wallet step](/how-tos/sign-a-wallet-step) for the queue's ordering rules.

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",
      "rebalance_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd",
      "step_sequence": 1,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
          "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": "0x02ed81893a85 ... 949c4e7f2b1d8a4e6cb3 ... 3a764000080c0",
      "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, build a `StepForSigning` from the webhook step and pass it to `LocalSigner.signStep`. It needs three values: the `unsigned_transaction`, the source wallet's on-chain address (`crypto_wallet_address` from `GET /v1/accounts/{estimated.from.account_id}`), and the network. Submit the returned `signature` to `POST /v1/treasury/rebalances/{rebalanceId}/steps/{stepId}/sign` to execute the rebalance step. See [Sign a Wallet Step](/how-tos/sign-a-wallet-step#sign-the-step-with-the-localsigner-sdk) for install and the full SDK reference.

<Tabs>
  <TabItem label="TypeScript">

```typescript
import { LocalSigner, type StepForSigning } from "@tesser-payments/sdk-ts";

const signer = new LocalSigner({
  signing: {
    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. Resolve the source wallet's on-chain address.
const account = await fetch(
  `https://api.tesser.xyz/v1/accounts/${step.estimated.from.account_id}`,
  { headers: { Authorization: `Bearer ${token}` } }
).then((res) => res.json());

// 3. Sign locally.
const toSign: StepForSigning = {
  unsignedTransaction: step.unsigned_transaction,
  signWith: account.crypto_wallet_address,
  network: step.estimated.from.network,
};
const { signature } = await signer.signStep(toSign);

// 4. Submit the signature to execute the step.
await fetch(
  `https://api.tesser.xyz/v1/treasury/rebalances/${step.rebalance_id}/steps/${step.id}/sign`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ signature }),
  }
);
```

  </TabItem>
  <TabItem label="Kotlin">

```kotlin
import kotlinx.coroutines.runBlocking
import xyz.tesser.sdk.LocalSigner
import xyz.tesser.sdk.SigningConfig
import xyz.tesser.sdk.StepForSigning

val signer = LocalSigner(
  SigningConfig(
    publicKey = System.getenv("SIGNING_PUBLIC_KEY"),
    privateKey = System.getenv("SIGNING_PRIVATE_KEY"),
    enclaveId = System.getenv("SIGNING_ENCLAVE_ID"),
  ),
)

runBlocking {
  // `step` is the step.signature_requested webhook's data.object (parsed).
  // `fetchAccount` / `submitSignature` wrap your own HTTP client.
  val signWith = fetchAccount(step.estimated.from.accountId).cryptoWalletAddress

  val toSign = StepForSigning(
    id = step.id,
    transferId = step.rebalanceId,
    unsignedTransaction = step.unsignedTransaction,
    signWith = signWith,
    network = step.estimated.from.network,
  )

  val signed = signer.signStep(toSign)

  // POST /v1/treasury/rebalances/{rebalanceId}/steps/{stepId}/sign
  submitSignature(toSign.transferId, toSign.id, signed.signature)
}
```

  </TabItem>
</Tabs>

`signStep` returns a `signature` to send to the rebalance step sign API to execute the step, plus `metadata` (`stampHeaderName`, `stampHeaderValue`, `body`) for debugging that stays client-side. (The Kotlin result also echoes `unsignedTransaction` for audit trails.)

Tesser then broadcasts the signed transaction. You will receive `step.signed`, followed by `step.submitted`, `step.confirmed` (with `transaction_hash` populated), and `step.completed`. For wallet-originated on-chain steps, `step.submitted` is an event-only marker — see [Step Statuses](/overviews/funds-movement-lifecycle-and-data-model#step-statuses) in the lifecycle overview.

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",
      "rebalance_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd",
      "step_sequence": 1,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
          "amount": "1000",
          "currency": "USDT",
          "network": "POLYGON"
        },
        "to": {
          "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f",
          "amount": "1000",
          "currency": "USDT",
          "network": "POLYGON"
        }
      },
      "actual": {
        "from": {
          "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
          "amount": "1000",
          "currency": "USDT",
          "network": "POLYGON"
        },
        "to": {
          "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f",
          "amount": "1000",
          "currency": "USDT",
          "network": "POLYGON"
        }
      },
      "fees": [],
      "transaction_hash": "0xb1c2d3e4f5a6 ... f9a0b1c2",
      "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
    }
  }
}
```

### Executing a Wallet-to-OpenFX Rebalance

A wallet-to-OpenFX rebalance uses the same signing flow as Scenario 3. Tesser emits `step.signature_requested` for the on-chain transfer, and you submit the signature through `POST /v1/treasury/rebalances/{rebalanceId}/steps/{stepId}/sign`. The transfer then moves through `step.signed`, `step.submitted`, and `step.confirmed`.

In production, OpenFX detects the transfer to its registered deposit address and sends the deposit webhook that completes the transfer step. In sandbox and staging, create the matching OpenFX sandbox deposit after the transfer reaches `submitted`, as described under [Wallet to OpenFX Ledger](#wallet-to-openfx-ledger). The transfer step completes only after Tesser receives the completed deposit webhook. Its `actual.to.amount` reflects the amount OpenFX reports as credited and can differ from `estimated.to.amount`. A same-currency rebalance then completes; a cross-currency rebalance continues to the OpenFX swap step using the credited amount.

## 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`.

<Tabs>
  <TabItem label="Scenario 1">

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",
        "rebalance_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": "2025-12-01T10:05:00.300Z",
        "confirmed_at": "2025-12-01T10:05:00.400Z",
        "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"
  }
}
```

  </TabItem>
  <TabItem label="Scenario 2">

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",
        "rebalance_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",
        "rebalance_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": "0x7e8c3f2a1b4d ... 7c8d9e0f",
        "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"
  }
}
```

  </TabItem>
  <TabItem label="Scenario 3">

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": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
        "amount": "1000",
        "currency": "USDT",
        "network": "POLYGON"
      },
      "to": {
        "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f",
        "amount": null,
        "currency": "USDT",
        "network": "POLYGON"
      }
    },
    "estimated": {
      "from": {
        "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
        "amount": "1000",
        "currency": "USDT",
        "network": "POLYGON"
      },
      "to": {
        "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f",
        "amount": "1000",
        "currency": "USDT",
        "network": "POLYGON"
      }
    },
    "actual": {
      "from": {
        "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
        "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",
        "rebalance_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd",
        "step_sequence": 1,
        "step_type": "transfer",
        "estimated": {
          "from": {
            "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
            "amount": "1000",
            "currency": "USDT",
            "network": "POLYGON"
          },
          "to": {
            "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f",
            "amount": "1000",
            "currency": "USDT",
            "network": "POLYGON"
          }
        },
        "actual": {
          "from": {
            "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
            "amount": "1000",
            "currency": "USDT",
            "network": "POLYGON"
          },
          "to": {
            "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f",
            "amount": "1000",
            "currency": "USDT",
            "network": "POLYGON"
          }
        },
        "fees": [],
        "transaction_hash": "0xb1c2d3e4f5a6 ... f9a0b1c2",
        "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"
  }
}
```

  </TabItem>
</Tabs>

## 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 | `step.submitted`, `step.confirmed`, and `step.completed` fire on the single step in close succession, followed by a `rebalance.updated` event carrying the full Rebalance with `actual.*` populated | Step 1: `actual.from.amount`, `actual.to.amount`, all timestamps. 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 broadcast and accepted into the network's mempool (not yet in a block) | `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` |
| 6 | The on-chain transaction is broadcast and accepted into the network's mempool (not yet in a block) | `step.confirmed` on the step | Step 1: `confirmed_at`, `transaction_hash` |
| 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`. |

**Additional route — Stablecoin from a self-custodial wallet to an OpenFX ledger**

| # | 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` with one transfer step for a same-currency route, or a transfer followed by a swap for a cross-currency route | Steps array; `estimated.from.amount` and `estimated.to.amount` |
| 3 | Tesser asks you to sign the on-chain transfer | `step.signature_requested` on the transfer step | Transfer step signing payload |
| 4 | You submit the signature | `POST /v1/treasury/rebalances/{rebalanceId}/steps/{stepId}/sign` with `{ "signature": "0x..." }`; `step.signed` and `rebalance.balance_updated` with `balance_status: "reserved"` fire | `balance_status`, `balance_reserved_at`; transfer step `status: "signed"` |
| 5 | Tesser broadcasts the signed transaction | `step.submitted` on the transfer step | Transfer step `submitted_at` |
| 6 | The transaction is accepted by the network | `step.confirmed` on the transfer step | Transfer step `confirmed_at`, `transaction_hash` |
| 7 | OpenFX credits the on-chain deposit. In sandbox and staging, create the matching OpenFX sandbox deposit after the transfer is submitted; production observes the deposit sent directly to OpenFX. | `step.completed` on the transfer step after Tesser receives the completed OpenFX deposit webhook; for a same-currency route, this is followed by `rebalance.updated` carrying the terminal Rebalance object | Transfer step `completed_at`, `actual.from.amount`, `actual.to.amount`; for a same-currency route, rebalance-level `actual.*` |
| 8 | For a cross-currency route, OpenFX executes the swap; a same-currency route is already complete | Cross-currency only: `step.submitted`, `step.confirmed`, and `step.completed` on the swap, followed by `rebalance.updated` | Swap `actual.*`; rebalance-level `actual.*` |

## 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, `step.submitted` fires (Tesser handed off to Circle) and the single `transfer` step then transitions to `failed` when Circle's response is a rejection rather than a confirmation. 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",
      "rebalance_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": "transfers-9301",
          "error_message": "The provider rejected the transfer step"
        }
      ],
      "created_at": "2025-12-01T10:00:00.000Z",
      "updated_at": "2025-12-01T10:30:00.000Z",
      "submitted_at": "2025-12-01T10:29:59.900Z",
      "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": "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",
          "rebalance_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": "transfers-9301",
              "error_message": "The provider rejected the transfer step"
            }
          ],
          "created_at": "2025-12-01T10:00:00.000Z",
          "updated_at": "2025-12-01T10:30:00.000Z",
          "submitted_at": "2025-12-01T10:29:59.900Z",
          "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.

<Tabs>
  <TabItem label="Step 1 (swap)">

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",
      "rebalance_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": "transfers-9304",
          "error_message": "The currency conversion trade failed"
        }
      ],
      "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"
    }
  }
}
```

  </TabItem>
  <TabItem label="Step 2 (wallet transfer)">

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",
      "rebalance_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": [],
      "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"
    }
  }
}
```

  </TabItem>
</Tabs>

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": "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",
          "rebalance_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": "transfers-9304",
              "error_message": "The currency conversion trade failed"
            }
          ],
          "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",
          "rebalance_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": [],
          "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",
      "rebalance_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": "transfers-9303",
          "error_message": "The transfer step failed to execute"
        }
      ],
      "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": "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": "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",
          "rebalance_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",
          "rebalance_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": "transfers-9303",
              "error_message": "The transfer step failed to execute"
            }
          ],
          "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"
    }
  }
}
```

### On-Chain Transfer Between Wallets Fails (Scenario 3)

If the on-chain wallet-to-wallet transfer in Scenario 3 fails after broadcast — for example because of a chain reorg, gas exhaustion, or a nonce conflict — the rebalance terminates with USDT remaining at the source wallet. Pre-broadcast failures (e.g., the signed step never reaches the chain) are surfaced by the step-signing API itself; see the [`/how-tos/sign-a-wallet-step`](/how-tos/sign-a-wallet-step) peer guide.

What you will observe:

- `step.failed` on Step 1 (wallet-to-wallet transfer). Step-level `actual.*` is null; `status_reasons` carries the failure detail. `transaction_hash` is present because the transfer broadcast onto the chain before failing.
- The rebalance's top-level `actual.*` is all null because no step reached `step.status = completed`.
- `balance_status` rolls back to `unreserved` and `balance_reserved_at` clears to `null` at terminal failure. The funds remain at the source wallet on-chain; the reservation that was held at step signing is released so the wallet's balance is no longer locked against this rebalance.
- A `rebalance.updated` webhook fires alongside the terminal `step.failed` event.

Example `step.failed` webhook on Step 1 (on-chain wallet transfer broadcast but not confirmed):

```json
{
  "id": "ec5a7d9f-3e2c-4561-b8d0-1f4e5a9c2b6d",
  "type": "step.failed",
  "created_at": "2025-12-01T11:00:00.000Z",
  "data": {
    "object": {
      "id": "3b7e9d2f-1c4a-4f68-a5b0-6e8c1d3f9a2b",
      "rebalance_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd",
      "step_sequence": 1,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
          "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": "0xa1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e",
      "provider_key": "turnkey",
      "status": "failed",
      "status_reasons": [
        {
          "error_code": "transfers-9305",
          "error_message": "The on-chain transaction failed to broadcast"
        }
      ],
      "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 3, on-chain transfer failed; USDT remains at the source wallet):

```json
{
  "id": "fa3e6c8b-7d2f-4561-9c0d-2e4f1b8d3a5c",
  "type": "rebalance.updated",
  "created_at": "2025-12-01T11:00:00.300Z",
  "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": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
          "amount": "1000",
          "currency": "USDT",
          "network": "POLYGON"
        },
        "to": {
          "account_id": "9c4e7f2b-1d8a-4e6c-b3f5-8a2d6e9b1c4f",
          "amount": null,
          "currency": "USDT",
          "network": "POLYGON"
        }
      },
      "estimated": {
        "from": {
          "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
          "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",
          "rebalance_id": "7c3d4e5f-6a7b-4901-bcde-3456789012cd",
          "step_sequence": 1,
          "step_type": "transfer",
          "estimated": {
            "from": {
              "account_id": "6ff06a5d-30eb-42ae-9f60-e5b7680c94f2",
              "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": "0xa1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e",
          "provider_key": "turnkey",
          "status": "failed",
          "status_reasons": [
            {
              "error_code": "transfers-9305",
              "error_message": "The on-chain transaction failed to broadcast"
            }
          ],
          "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.300Z",
      "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` when the async balance check runs (Scenarios 1 and 2), Tesser fires `rebalance.balance_updated` with `balance_status: "awaiting_funds"` shortly after `rebalance.quote_created` 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). For wallet sources (Scenario 3 and wallet-to-OpenFX rebalances), insufficient funds surface as a 4XX from the step-signing API; `rebalance.balance_updated` fires shortly afterward, and your retry path is to fund the wallet and call sign again. If the reservation does not succeed before `expires_at`, the rebalance times out.

What you will observe:

- `rebalance.balance_updated` with `balance_status: "awaiting_funds"` shortly after `rebalance.quote_created` (ledger sources) or shortly after a rejected sign call (wallet sources).
- (Optional) further `rebalance.balance_updated` events as balance changes are detected.
- At `expires_at`: `step.failed` on every step that was still in `created` status. Step-level `actual.*` is null; `status_reasons` is `[]` because the step never started — the failure cause lives at the resource level (the rebalance timed out after remaining in `awaiting_funds` until `expires_at`).
- The rebalance's top-level `actual.*` is all null because no step reached `step.status = completed` — no funds ever moved.
- A terminal `rebalance.updated` webhook fires alongside the `step.failed` events, carrying the full updated Rebalance object reflecting the terminal state.

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",
          "rebalance_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": "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",
          "rebalance_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": [],
          "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"
    }
  }
}
```
