# Rebalance Funds

Rebalances move funds between managed accounts on the Tesser platform. Rebalances can be same-currency as well as cross-currency (that include a swap step). Common patterns include moving funds between two ledgers at the same provider, between a provider ledger and a self-custodial wallet, or between two self-custodial wallets.

Before creating a rebalance, review the [Funds Movement Lifecycle and Data Model](/overviews/funds-movement-lifecycle-and-data-model) overview to understand the shared data shape, lifecycle phases, and statuses that apply to rebalances and Tesser's other funds-movement resources.

## Prerequisites

The `desired.from.account_id` and `desired.to.account_id` must each be one of your managed accounts — either a provider ledger (e.g., a Circle ledger or an OpenFX ledger) or a self-custodial wallet you have registered with Tesser. For more information on registering accounts, see [Create an account](/how-tos/create-an-account).

Rebalances are first-party only — they move funds between your own managed accounts and never involve a third party. If you want to send funds to a third party, see [Send a Stablecoin Payout](/how-tos/send-a-stablecoin-payout/create-a-stablecoin-payout) and [Create a counterparty](/how-tos/create-a-counterparty).

For Scenario 2 in this guide, the source ledger is at OpenFX and assumes USD has already landed at that ledger via a fiat deposit. See [Deposit Funds via a Liquidity Provider](/how-tos/deposit-funds-via-a-liquidity-provider) for the earlier part of the deposit lifecycle.

## Rebalance Workflow

A rebalance executes as one or more steps. Tesser plans the route 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. Cross-currency rebalances start with a swap step at a provider ledger, then transfer the swapped balance.

For a complete description of how rebalances move through planning, balance reservation, execution, and terminal state, see the lifecycle overview's [Planning](/overviews/funds-movement-lifecycle-and-data-model#planning), [Execution](/overviews/funds-movement-lifecycle-and-data-model#execution), and [Terminal State and Divergence](/overviews/funds-movement-lifecycle-and-data-model#terminal-state-and-divergence) sections. Step statuses are documented under [Step Statuses](/overviews/funds-movement-lifecycle-and-data-model#step-statuses).

## Exchange Rates for Cross-Token Rebalances

When a rebalance crosses currencies, Tesser sources an indicative quote from the relevant liquidity provider and reports it via the `estimated.from.amount` and `estimated.to.amount` fields. The actual fill rate may differ slightly from the indicative quote and is reflected in `actual.*` once the swap step completes. See [Planning](/overviews/funds-movement-lifecycle-and-data-model#planning) for how Tesser obtains and reports quotes.

| Type of liquidity provider | Example | Behavior |
|---|---|---|
| On/Off ramp | OpenFX | Exchange rate between buy and sell currencies fluctuates; current rates can be guaranteed for a period of time |
| Exchange | Kraken | Exchange rate fluctuates; no guaranteed rate |

For same-currency rebalances, `estimated.*` matches `desired.*` (1:1) and there is no swap step.

## Rebalance Creation

Submit a request to [POST /v1/treasury/rebalances](/api/treasury#create-rebalance).

- If applicable, you should specify on which `tenant`'s behalf you are requesting the rebalance.
- For the rebalance, populate the following fields in the `desired` object:
    - `desired.from.account_id`: The identifier of the account funds will be moved from.
    - `desired.from.amount`: The amount of `desired.from.currency` to move from the source.
    - `desired.from.currency`: The currency to move from the source.
    - `desired.from.network`: The network of the funds at `desired.from.account_id` (only applicable when `desired.from.account_id` is a wallet).
    - `desired.to.account_id`: The identifier of the destination account.
    - `desired.to.currency`: The currency to land at the destination.
    - `desired.to.network`: The destination network (only applicable when the destination account is a wallet).

*Note: `desired.to.amount` is not auto-populated. The Rebalance's `desired.to.amount` remains `null` throughout the lifecycle; the indicative target amount is supplied in `estimated.to.amount` once the quote is obtained.*

**The examples in this guide show 3 scenarios:**

1. Same-token rebalance between two ledger accounts at Circle (USDC, no network)
2. Cross-token rebalance from a USD ledger at OpenFX to a self-custodial wallet on BASE (USDC)
3. Same-token rebalance between two self-custodial wallets (USDT on POLYGON)

<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):** 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 with `transaction_hash: null` and the step stays `signed`. 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
    }
  }
}
```

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

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