# Funds Movement Lifecycle and Data Model

Tesser models funds movement using a single shared data shape and lifecycle. The resource types differ mainly in their **direction** and which **pre-execution checks** apply. Their data model, step structure, status taxonomy, and webhook conventions are the same.

## Resource Types

Tesser exposes four funds-movement resources (payments, deposits, withdrawals, and rebalances). Each is modeled with the same shared envelope (described in [Resource Data Model](#resource-data-model)) and progresses through the same [Lifecycle Phases](#lifecycle-phases).

- **Payment** — funds movement to or from an external counterparty. Payouts (`direction` = outbound) are created via [POST /v1/payments](/api/payments#create-payment). Inbound payments (`direction` = inbound) cannot currently be pre-registered but will be recorded and surfaced via webhooks and the API.
- **Deposit** — inbound funds movement that on-ramps fiat from your bank to a Tesser-managed account, with optional conversion into stablecoin at a liquidity provider. Created via [POST /v1/treasury/deposits](/api/treasury#create-deposit).
- **Withdrawal** — outbound funds movement that off-ramps stablecoin held at Tesser-managed accounts back to your bank. Created via [POST /v1/treasury/withdrawals](/api/treasury#create-withdrawal).
- **Rebalance** — internal funds movement between two managed accounts (provider ledger to wallet, wallet to wallet, ledger to ledger). Created via [POST /v1/treasury/rebalances](/api/treasury#create-rebalance).

## Resource Data Model

Each resource type is represented as a single JSON object with a consistent set of top-level fields, a `desired` / `estimated` / `actual` overlay, and the `steps[]` array. Resource-specific fields (`risk_status`, `balance_status`, `funding_account_id`) are added as applicable at the top-level of the record — see the [Resource-Specific fields](#resource-specific-fields) for which applies where.

### Shared Top-Level Fields

All four resource types include the following top-level fields:

- `id` — Tesser-assigned UUID for the resource.
- `workspace_id` — UUID of the workspace that owns the resource.
- `organization_reference_id` — optional client-supplied identifier (e.g., your internal payment or treasury operation ID).
- `direction` — one of `outbound`, `inbound`, or `rebalance`. See the matrix below for which value each resource takes.
- `created_at` — timestamp the resource was created at Tesser.
- `updated_at` — timestamp of the most recent change to the resource.
- `expires_at` — timestamp after which the resource will no longer execute. See [Expiration](#expiration).
- `steps[]` — ordered array of execution steps Tesser plans for the resource. Populated shortly after creation and supplied via the `<resource>.quote_created` webhook.
- `desired` / `estimated` / `actual` — the [overlay](#the-desired--estimated--actual-overlay) describing intent, projection, and outcome. Each contains `from` and `to` fields.

### Resource-Specific Fields

Some fields and concepts apply only to a subset of resource types. The table below summarizes which top-level concepts appear on which resources.

| Field / Concept | Payment | Deposit | Withdrawal | Rebalance |
|---|---|---|---|---|
| `risk_status` (risk check) | yes | no | no | no |
| `balance_status` (balance check) | yes | no | yes | yes |
| `funding_account_id` | yes | no | no | no |
| `direction` value | `outbound`, `inbound` | `inbound` | `outbound` | `rebalance` |

- **Risk check** screens the external sending or receiving wallet of a payment. 
- **Balance check** reserves funds in the `desired.from.account_id`; applied whenever funds are debited from a managed account (outbound payments, withdrawals, rebalances).
- **`funding_account_id`** is unique to outbound payments — it identifies which source bank account ultimately funds the payout. See [Travel Rule](/overviews/compliance-and-risk#travel-rule).

The skeletons below show how those resource-specific differences manifest in the top-level shape of each non-payment resource. Overlay sub-objects and `steps[]` are elided — see [The Desired / Estimated / Actual Overlay](#the-desired--estimated--actual-overlay) below for the overlay shape and the [full payment example](#example-outbound-payment) further down for everything in context.

<Tabs>
  <TabItem label="Deposit">

`direction: "inbound"`. No risk fields, no `balance_status`, no `funding_account_id`.

```jsonc
{
  "id": "1c8e4a6f-9b2d-4f53-a0e7-5d3c1b9f2a8e",
  "workspace_id": "b53f6690-3242-4942-9907-885779632832",
  "organization_reference_id": null,
  "direction": "inbound",
  "desired":   { /* from / to */ },
  "estimated": { /* from / to */ },
  "actual":    { /* from / to */ },
  "steps":     [ /* one or more step objects */ ],
  "created_at": "2025-12-02T14:00:00.000Z",
  "updated_at": "2025-12-02T14:30:00.700Z",
  "expires_at": "2025-12-03T14:00:00.000Z"
}
```

  </TabItem>
  <TabItem label="Withdrawal">

`direction: "outbound"`. Adds `balance_status` and `balance_reserved_at`. No risk fields, no `funding_account_id`.

```jsonc
{
  "id": "4f8c2e9a-1b6d-4a37-8e5f-3c9d2a7b1e6f",
  "workspace_id": "b53f6690-3242-4942-9907-885779632832",
  "organization_reference_id": null,
  "direction": "outbound",
  "balance_status": "reserved",
  "balance_reserved_at": "2025-12-01T10:00:01.000Z",
  "desired":   { /* from / to */ },
  "estimated": { /* from / to */ },
  "actual":    { /* from / to */ },
  "steps":     [ /* one or more step objects */ ],
  "created_at": "2025-12-01T10:00:00.000Z",
  "updated_at": "2025-12-01T14:00:00.300Z",
  "expires_at": "2025-12-02T10:00:00.000Z"
}
```

  </TabItem>
  <TabItem label="Rebalance">

`direction: "rebalance"`. Adds `balance_status` and `balance_reserved_at`. No risk fields, no `funding_account_id`.

```jsonc
{
  "id": "9a1b2c3d-4e5f-4789-a0bc-1234567890ab",
  "workspace_id": "b53f6690-3242-4942-9907-885779632832",
  "organization_reference_id": null,
  "direction": "rebalance",
  "balance_status": "reserved",
  "balance_reserved_at": "2025-12-01T10:00:01.000Z",
  "desired":   { /* from / to */ },
  "estimated": { /* from / to */ },
  "actual":    { /* from / to */ },
  "steps":     [ /* one or more step objects */ ],
  "created_at": "2025-12-01T10:00:00.000Z",
  "updated_at": "2025-12-01T10:30:00.200Z",
  "expires_at": "2025-12-02T10:00:00.000Z"
}
```

  </TabItem>
</Tabs>

By comparison, the [outbound payment example](#example-outbound-payment) below adds `risk_status`, `risk_status_reasons`, `risk_reviewed_by`, `risk_reviewed_at`, `funding_account_id`, plus `balance_status` and `balance_reserved_at` at the top level.

### The Desired / Estimated / Actual Overlay

Funds movement at scale involves uncertainty: liquidity providers may not guarantee exchange rates, on-chain transactions may fail, and resources can terminate at intermediate steps if they don't complete by `expires_at`. To handle this gracefully, every resource carries three parallel snapshots of its `from` and `to` fields.

- **Desired** — your stated intent at creation: the source and destination accounts, currencies, networks, and amount. Set on the create request and **never overwritten** afterward. This preserves intent for downstream reconciliation regardless of how the resource ultimately concludes.
- **Estimated** — Tesser's best projection for how the resource will proceed if it succeeds. Populated after planning and quoting (see [Planning](#planning)). For resources that involve a swap or cross-currency step, the ratio of `estimated.from.amount` to `estimated.to.amount` is the indicative exchange rate. For same-currency moves, `estimated.*` matches `desired.*` (1:1).
- **Actual** — what actually settled. Populated as steps complete and at the resource level when the resource reaches a terminal state. On success, `actual.*` matches `estimated.*`. On failure, `actual.*` reflects the final state — possibly different account, currency, or network than `desired.*`.

Each overlay contains a `from` and `to` field with the same four sub-fields:

- `account_id` — the Tesser account UUID for that step.
- `amount` — string-encoded decimal amount.
- `currency` — currency code (e.g., `USD`, `USDC`, `MXN`).
- `network` — blockchain network code (e.g., `BASE`, `POLYGON`), or `null` for non-network steps (bank accounts, provider ledgers).

#### Overlay applicability
The desired fields are only included on the top-level of the record. The estimated and actual overlay structure is repeated on each step.

| Overlay | Top-Level | Step |
|---|---|---|
| Desired | Yes | No |
| Estimated | Yes | Yes|
| Actual | Yes | Yes|

The `estimated.from.*` fields at the top level of the record are populated from the `desired.from.*` fields. The `estimated.to.*` fields represent Tesser's best estimate for the outcome of last step of the resource. 

### Steps

The `steps[]` array represents the ordered sequence of steps Tesser plans to execute. Each step has the `estimated` and `actual` overlay, plus a `status` and timestamps for each phase transition.

Two step types are currently used across all resource types:

- `transfer` — funds move from one account to another. The source and destination accounts may differ in currency or network. Examples: an on-chain stablecoin transfer between two wallets; a fiat push from a bank to a liquidity provider's bank; a last-mile fiat payout via a local payment network.
- `swap` — currencies are exchanged within the same account (the step's `estimated.from.account_id` and `estimated.to.account_id` are equal). Example: at a liquidity provider, selling USD and buying USDC inside the same provider ledger.

Each step has a status. See [Step Statuses](#step-statuses) for more information.

### Example: Outbound Payment

The JSON below shows the full shape of a successful USDC (Ethereum) → MXN payout, captured at terminal state. Note how the `desired` overlay appears only at the top level, while `estimated` and `actual` repeat on each step in `steps[]`. Resource-specific fields (`risk_status`, `balance_status`, `funding_account_id`) are all populated for outbound payments — compare against the deposit, withdrawal, and rebalance skeletons in [Resource-Specific Fields](#resource-specific-fields) above. Because this payout routes through a stablecoin off-ramp provider that quotes a deterministic delivery amount, `actual.to.amount` matches `estimated.to.amount` exactly; the only overlay divergence visible is on the source side, where `desired.from.amount` is `null` (the request fixed the `to.amount`) and `estimated.from.amount` carries Tesser's quote.

<details className="collapsible-example">
<summary>View full JSON resource</summary>

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440020",
  "workspace_id": "550e8400-e29b-41d4-a716-446655440001",
  "organization_reference_id": "ref_123",
  "direction": "outbound",
  "funding_account_id": "550e8400-e29b-41d4-a716-446655440010",
  "desired": {
    "from": {
      "account_id": "550e8400-e29b-41d4-a716-446655440011",
      "amount": null,
      "currency": "USDC",
      "network": "ETHEREUM"
    },
    "to": {
      "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047",
      "amount": "1000",
      "currency": "MXN",
      "network": null
    }
  },
  "estimated": {
    "from": {
      "account_id": "550e8400-e29b-41d4-a716-446655440011",
      "amount": "55.85",
      "currency": "USDC",
      "network": "ETHEREUM"
    },
    "to": {
      "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047",
      "amount": "1000",
      "currency": "MXN",
      "network": null
    }
  },
  "actual": {
    "from": {
      "account_id": "550e8400-e29b-41d4-a716-446655440011",
      "amount": "55.85",
      "currency": "USDC",
      "network": "ETHEREUM"
    },
    "to": {
      "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047",
      "amount": "1000",
      "currency": "MXN",
      "network": null
    }
  },
  "risk_status": "automatically_approved",
  "risk_status_reasons": [],
  "participants": [],
  "provider_metadata": [
    {
      "provider_metadata_type": "WALLET_RISK_SCREENING",
      "provider_metadata_key": "TRM_LABS",
      "provider_metadata_data": {
        "address": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
        "entities": [],
        "addressRiskIndicators": []
      }
    }
  ],
  "risk_reviewed_by": null,
  "risk_reviewed_at": null,
  "balance_status": "reserved",
  "balance_reserved_at": "2025-12-01T09:00:01.000Z",
  "steps": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440100",
      "payment_id": "550e8400-e29b-41d4-a716-446655440020",
      "step_sequence": 1,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "550e8400-e29b-41d4-a716-446655440011",
          "amount": "55.85",
          "currency": "USDC",
          "network": "ETHEREUM"
        },
        "to": {
          "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
          "amount": "55.85",
          "currency": "USDC",
          "network": "ETHEREUM"
        }
      },
      "actual": {
        "from": {
          "account_id": "550e8400-e29b-41d4-a716-446655440011",
          "amount": "55.85",
          "currency": "USDC",
          "network": "ETHEREUM"
        },
        "to": {
          "account_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
          "amount": "55.85",
          "currency": "USDC",
          "network": "ETHEREUM"
        }
      },
      "transaction_hash": "0x1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f",
      "unsigned_transaction": null,
      "fees": [
        {
          "fee_amount": "0.01",
          "fee_currency": "USDC",
          "fee_type": "gas",
          "fee_metadata": {}
        }
      ],
      "provider_key": "turnkey",
      "status": "completed",
      "status_reasons": [],
      "created_at": "2025-12-01T09:00:00.400Z",
      "updated_at": "2025-12-01T09:00:02.000Z",
      "signature_requested_at": "2025-12-01T09:00:00.800Z",
      "signed_at": "2025-12-01T09:00:01.050Z",
      "submitted_at": "2025-12-01T09:00:01.200Z",
      "confirmed_at": "2025-12-01T09:00:01.800Z",
      "completed_at": "2025-12-01T09:00:02.000Z",
      "failed_at": null
    },
    {
      "id": "a7b3d912-5f8e-4c23-9d6a-1e4f7b2c8a5d",
      "payment_id": "550e8400-e29b-41d4-a716-446655440020",
      "step_sequence": 2,
      "step_type": "transfer",
      "estimated": {
        "from": {
          "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b",
          "amount": "55.85",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047",
          "amount": "1000",
          "currency": "MXN",
          "network": null
        }
      },
      "actual": {
        "from": {
          "account_id": "f3a8b2c1-7d4e-4f9a-b6e5-2c8d9a1f0e3b",
          "amount": "55.85",
          "currency": "USDC",
          "network": null
        },
        "to": {
          "account_id": "a3f7c891-bd42-4e19-9c5a-2d8b6f3e1047",
          "amount": "1000",
          "currency": "MXN",
          "network": null
        }
      },
      "transaction_hash": null,
      "unsigned_transaction": null,
      "fees": [
        {
          "fee_amount": "1.50",
          "fee_currency": "USDC",
          "fee_type": "provider",
          "fee_metadata": {}
        }
      ],
      "provider_key": "openfx",
      "status": "completed",
      "status_reasons": [],
      "created_at": "2025-12-01T09:00:00.040Z",
      "updated_at": "2025-12-01T09:00:02.500Z",
      "signature_requested_at": null,
      "signed_at": null,
      "submitted_at": "2025-12-01T09:00:02.100Z",
      "confirmed_at": "2025-12-01T09:00:02.300Z",
      "completed_at": "2025-12-01T09:00:02.500Z",
      "failed_at": null
    }
  ],
  "created_at": "2025-12-01T09:00:00.000Z",
  "updated_at": "2025-12-01T09:00:02.500Z",
  "expires_at": "2025-12-01T23:59:59.999Z"
}
```

</details>

For the full webhook sequence leading to this terminal state, see [Create a payout from a wallet](/how-tos/send-a-stablecoin-payout/create-a-payout-from-a-wallet).

### Fees

Fees are reported as an array in the per-step `fees[]` object. Fees include blockchain network (gas) fees or provider fees. Each entry includes:
- `fee_amount`
- `fee_currency`
- `fee_type`
- (Optional) `fee_metadata`

:::note
Depending on the type, the amount of a fee may not be known until the step has executed (e.g. gas fees). Other times, the fee may be known in advance (e.g. if a fiat off-ramp charges a separate transaction fee for a payout).
:::

## Lifecycle Phases

Every resource progresses through four phases: planning, pre-execution checks, execution, and terminal state. Each phase emits webhooks; the prefix is the resource type (`payment.*`, `deposit.*`, `withdrawal.*`, `rebalance.*`). 

### Planning

When you POST the create request, Tesser creates the resource synchronously and begins to plan the sequence of steps to execute the request, including the quote. 

The quote includes:

- **Exchange rate** — The ratio of `estimated.from.amount` to `estimated.to.amount` is the indicative exchange rate between `estimated.from.currency` and `estimated.to.currency`.
    - For stablecoin-to-stablecoin same-currency transfers, the rate is always 1:1. 
    - For cross-currency transfers, Tesser sources the best exchange rate from available liquidity providers.
- **From or To amount** — For payments, you may request a quote based on the amount to send (specify `desired.from.amount`) or the to amount to receive (specify `desired.to.amount`). Tesser determines the other side of the overlay based on the prevailing rate. For deposits, withdrawals, and rebalances, you specify the `desired.from.amount`. 

Tesser then fires the `<resource>.quote_created` webhook. This event **always fires**, even for same-token, same-network moves, to keep the lifecycle uniform across resource types.

The `<resource>.quote_created` webhook contains the planned `steps[]` array. Each step has a `status` of `created`. At this point the `desired` overlay is populated on the top-level of the resource. The `estimated` overlay is populated at the top level and on each step. `actual.*` fields are all null.

### Pre-Execution Checks

Before steps begin executing, Tesser runs the checks that apply to the resource (per the [Resource-Specific fields](#resource-specific-fields) above).

- **Risk check** (outbound payments only, as a pre-execution gate) — screens the destination wallet. The `payment.risk_updated` webhook reports the outcome via `risk_status` (see [Risk Statuses](#risk-statuses)). The outbound payment will not proceed to step signing until risk is approved (`automatically_approved` or `manually_approved`). If your organization's policy requires manual review for the result, a user with sufficient permissions can review the payment in the Tesser dashboard or submit a decision via [`POST /v1/payments/{paymentId}/review`](/api/payments#submit-risk-review). Inbound payments also undergo risk screening — but because the funds have already arrived on-chain, the screening is not a pre-execution gate. See [Receive an Inbound Payment to a Wallet](/how-tos/receive-an-inbound-payment-to-a-wallet#inbound-payment-risk-review-paymentrisk_updated) for the inbound-specific timing and what it gates.

- **Balance check** (payments, withdrawals, rebalances) — reserves source funds on the resource's source account. Two outcomes are possible: `reserved` (sufficient funds; the resource proceeds to execution) or `awaiting_funds` (insufficient funds; the resource is queued and will retry until `expires_at`). Reported via `<resource>.balance_updated`.

Where the source account is a **provider ledger** (e.g. Circle ledger, OpenFX ledger), Tesser performs the balance check automatically by reading the ledger's available balance.

Where the source account is a **self-custodial wallet**, the balance check is part of the step-signing process: Tesser emits `step.signature_requested`, you sign the step locally and submit the signature, and the synchronous response plus the subsequent `<resource>.balance_updated` webhook reflect the reservation outcome.

### Execution

Once pre-execution checks pass, Tesser executes each step in order. Steps within the same resource may overlap (a fiat off-ramp step may begin before a preceding on-chain step has fully finalized), but each individual step transitions through the same status states in sequence.

The step status progression when on-chain signing is not applicable is:

```
created → submitted → confirmed → completed
```

Any state after `created` (i.e., `submitted` or `confirmed`) may transition to `failed`.

The step status path when on-chain signing is required is:

```
created → signature_requested → signed → submitted → confirmed → completed
```

Any state after `created` (i.e., `signature_requested`, `signed`, `submitted`, or `confirmed`) may transition to `failed`.

Each transition emits a `step.<status>` webhook. See [Step Statuses](#step-statuses) for definitions.

:::note
Sometimes, a step's `status` will progress directly from `created` to `completed` or `failed`. This can happen when providers emit limited webhooks to Tesser, which reduces the visibility Tesser has to granular intermediate movements.
:::


### Terminal State and Divergence

When the last step reaches a terminal state, Tesser populates the top-level `actual.*` overlay and emits a `<resource>.updated` webhook carrying the full updated resource object. You can use this webhook to observe the terminal state without GET-ing the resource.

- **`completed`** — `actual.from.*` and `actual.to.*` are populated. Where the resource includes a swap step, the actual exchange rate may differ slightly from the indicative quote.
- **`failed`** — `actual.*` reflects what actually occurred. The resource may have terminated mid-route, leaving funds at an intermediate account in an intermediate currency. For example, an on-ramp deposit that fails at its swap step terminates with `actual.to.currency = "USD"` (fiat at the provider's ledger) even though `desired.to.currency = "USDT"`.

## Statuses Reference

This section catalogs the status taxonomies used across the lifecycle.

### Risk Statuses

Result of risk review performed on the external counterparty wallet of a payment — the destination wallet for outbound payments, the sending wallet for inbound payments. Risk statuses appear on payments only; see the [Resource-Specific fields](#resource-specific-fields). `risk_status_reasons` is populated with the reason codes when the status is `awaiting_decision`, `automatically_rejected`, or `manually_rejected`; it stays empty on approvals.

| Status | Terminal | Webhook event type | Description |
|---|---|---|---|
| `unchecked` | No | Not sent. | Initial state on creation; risk screening has not yet been initiated. |
| `awaiting_decision` | No | `payment.risk_updated` | The external wallet has been risk screened and requires manual review to determine whether the payment may proceed. |
| `automatically_approved` | Yes | `payment.risk_updated` | The external wallet has been risk screened and automatically approved per your organization's policy. |
| `manually_approved` | Yes | `payment.risk_updated` | The external wallet has been risk screened and has been manually reviewed and approved. |
| `automatically_rejected` | Yes | `payment.risk_updated` | The external wallet has been risk screened and automatically rejected per your organization's policy. |
| `manually_rejected` | Yes | `payment.risk_updated` | The external wallet has been risk screened and has been manually reviewed and rejected. |

### Balance Statuses

Result of the balance check on the source account. Balance statuses appear on payments, withdrawals, and rebalances; see the [Resource-Specific fields](#resource-specific-fields). The webhook event type uses the resource-type prefix — e.g. `payment.balance_updated`, `withdrawal.balance_updated`, `rebalance.balance_updated`.

| Status | Terminal | Webhook event type | Description |
|---|---|---|---|
| `unreserved` | No | Not sent. All resources at creation have a balance status of `unreserved`. | Source account has not been supplied or the reserve operation has not yet completed. |
| `awaiting_funds` | No | `<resource>.balance_updated` | The balance of the source account was checked and there are insufficient funds. The resource is queued and awaits funding (e.g., from a deposit) until `expires_at`. |
| `reserved` | Yes | `<resource>.balance_updated` | The balance of the source account was checked and funds were reserved to process the resource. |

### Step Statuses

Result of step execution. Step statuses and their webhooks (`step.*`) are shared across all four resource types and all step types (`transfer`, `swap`, future types).

| Status | Terminal | Webhook event type | Description |
|---|---|---|---|
| `created` | No | Not sent. All steps at creation have a status of `created`. | Tesser has created a record for this step. |
| `signature_requested` | No | `step.signature_requested` | Signature has been requested for the step. Only applicable for self-custodial wallets. |
| `signed` | No | `step.signed` | Step has been cryptographically signed. Only applicable for self-custodial wallets. |
| `submitted` | No | `step.submitted` | Tesser submitted the step information to the blockchain or fiat payment network. |
| `confirmed` | No | `step.confirmed` | The step was accepted by the network operator. For on-chain transfer steps, the transaction has been broadcast and accepted into the network's mempool — it is not yet included in a block, and can dwell here indefinitely (low gas, nonce conflict, mempool eviction); `step.completed` is the finality marker, not this. For off-chain and ledger-internal steps, this marks provider-side acknowledgment. |
| `completed` | Yes | `step.completed` | The step delivered funds to the destination account. For on-chain steps, the transaction has been included in a block and reached the network's confirmation threshold. For fiat steps, indicates the local payment network has delivered funds. Some fiat networks may not provide formal confirmation, in which case funds are assumed delivered unless `failed` is indicated. |
| `failed` | Yes | `step.failed` | The step was not successful; funds were not transferred from the source to the destination. |

Some step instances depart from this canonical sequence: in one pattern, intermediate webhooks don't fire at all; in another, all webhooks fire but the recorded step state skips an intermediate stage. The notes below describe each.

:::note
For customer-initiated steps, e.g. fiat funds transfers to liquidity providers, only `step.completed` (or `step.failed`) fires. Tesser doesn't emit intermediate `step.submitted` or `step.confirmed` events; the step's `submitted_at` is null while `confirmed_at` populates from the provider notification timestamp. See [Execution](#execution).
:::

:::note
For self-custodial wallet-originated on-chain steps (provider `turnkey`), `step.submitted` is an event-only marker. Tesser fires the webhook when it broadcasts the signed transaction, and the step state transitions directly from `signed` to `confirmed` when the broadcast result lands. The `step.submitted` payload and subsequent GET responses still surface `status: "submitted"` and a populated `submitted_at`, so the lifecycle reads the same as other providers.
:::

:::note{title="Inbound payments confirm differently"}
The on-chain semantics above describe steps Tesser **initiates**: Tesser broadcasts the transaction, so `step.confirmed` marks mempool acceptance and `step.completed` marks block inclusion and finality. **Inbound payments** are the reverse — Tesser only **observes** a transfer it didn't send, and first becomes aware of it once the transaction has already been included in a block. For an inbound step, `step.confirmed` therefore means the transaction is **in a block but not yet finalized**, and `step.completed` means the block has reached finality. There is no mempool stage to report, because Tesser never held the transaction before it was mined. See [Receive an Inbound Payment to a Wallet](/how-tos/receive-an-inbound-payment-to-a-wallet) for the full inbound lifecycle.
:::

## Expiration

Every resource has an `expires_at` timestamp. The validity period varies by resource type, currency pair, route, and amount. After expiration, the resource will not execute and a new resource must be created to retry.

:::warning{title="Why resources expire"}
Even same-currency, same-network resources have an `expires_at`. Risk and compliance checks must be re-run after a certain amount of time has passed if the resource has not been executed, and cross-currency transfers need their quotes refreshed.
:::

A resource enters the expired state when its `expires_at` passes before it has reached a completed terminal state. This can happen when steps were planned but didn't all reach `completed` in time, or when steps were never planned at all — for example, when no provider quote could be sourced before expiration. Concrete examples: a payment whose `balance_status` never reached `reserved` because the `desired.from.account_id` was not replenished in time; a deposit whose terminal step never confirmed; a withdrawal that expired before any provider quote was available.

When this happens, Tesser emits a `<resource>.expired` webhook (`payment.expired`, `deposit.expired`, `withdrawal.expired`, `rebalance.expired`) carrying the resource's final state. The `actual.*` overlay on the payload reflects the last observed state. You can use this webhook to observe the expiration without GET-ing the resource — see [Payment Updates](/webhooks/payment-updates) and [Treasury Updates](/webhooks/treasury-updates) for the full event tables.

To retry, create a new resource with fresh `desired.*` fields; you cannot resurrect an expired resource.
