# Receive an Inbound Payment to a Wallet

An inbound payment is a Tesser Payment resource with `direction = "inbound"`. It represents funds sent on-chain from an external wallet to one of your managed accounts at Tesser. This guide covers the wallet-recipient flow: inbound payments credited to a managed wallet at Tesser. Before reading further, review the [Funds Movement Lifecycle and Data Model](/overviews/funds-movement-lifecycle-and-data-model) for the shared data shape, lifecycle phases, and status taxonomy that apply to inbound payments along with Tesser's other funds-movement resources, and [Compliance and Risk Management](/overviews/compliance-and-risk) for the two compliance checks — Travel Rule data exchange and wallet risk screening — that shape the inbound lifecycle.

## What's Different About Inbound Payments

Inbound payments share the Payment resource shape with outbound payments but diverge in several ways. Note these before you begin your integration:

- **Two entry paths.** An inbound payment either begins with a Travel Rule authorization request from the originator VASP, in which case the payment record is created *before funds move*, or arrives as a direct on-chain transfer, in which case the record is created when Tesser sees the transaction in a block. See [How an Inbound Payment Begins](#how-an-inbound-payment-begins).
- **No customer-side planning.** The `desired` overlay does not populate. `estimated` populates when Tesser records the inbound payment; `actual` populates only when the block finalizes (at `step.completed` / `payment.updated`). There is no quote, no `payment.quote_created` webhook, and no `PATCH` step.
- **No balance check.** `balance_status` is `null` for the life of an inbound payment. Funds are being _credited_ to the wallet's balance, not debited from it, so there is no balance to reserve and no `unreserved` → `reserved` transition to report.
- **No signing.** Your integration never calls a sign endpoint for an inbound payment. The on-chain transaction was signed by the external sender, not by you.
- **Exactly one step.** The payment contains exactly one step, of type `transfer`. Inbound payments get created with step `status` as `created` when the record predates the transaction (a Travel Rule authorization flow) and with step `status` as `confirmed` when the record is created from a direct on-chain transfer without the Travel Rule flow. Step `status` will be `confirmed` once the transaction is in a block, `completed` once the block finalizes, and `failed` in the case where the funds do not settle — a rejected authorization, a sender who never broadcasts, or a block that never finalizes.
- **No expiration.** `expires_at` is always `null` on an inbound payment. You are receiving funds on-chain rather than instructing a payment, so there is nothing to expire.
- **The originator is identified through `participants[]`, not supplied by you.** Tesser reconciles the originating participant, and the VASP hosting the sending wallet, if there is one, against your counterparty and tenant records, and records the outcome in the payment's `participants[]` array. See [How the Sending Wallet Is Identified](#how-the-sending-wallet-is-identified).
- **`funding_account_id` is `null`.** Inbound records identify the sender through `estimated.from.account_id` and `participants[]`, never through `funding_account_id`.
- **Risk screening targets the sender and is advisory.** Because the recipient is your managed wallet, Tesser screens the external sending wallet against your organization's risk policy and notifies you of the outcome via `payment.risk_updated`. Screening runs in parallel with the on-chain lifecycle and does not gate the credit — the receiving wallet's balance updates when the transaction is confirmed in a block, regardless of the risk outcome.

## How an Inbound Payment Begins

An inbound payment can begin one of two ways:

- **Through a Travel Rule authorization flow.** The sending (originator) VASP requests authorization through a Travel Rule data-exchange provider *before* broadcasting the transaction on-chain, and exchanges identity information as part of that request. Tesser creates a payment record and populates it with the data available from the Travel Rule provider. 
- **As a direct on-chain transfer.** A sending wallet pushes funds to the wallet with no prior authorization request. No Travel Rule data accompanies the transfer, so Tesser creates the payment record when it sees the transaction in a block and infers what it can about the sender from screening the sending wallet through a blockchain analytics provider.

<Mermaid
  chart={`flowchart TD
  A[Inbound payment initiated] --> B{How did the<br/>payment initiate?}
  B -->|Travel Rule<br/>authorization request| C[Travel Rule<br/>pre-authorization checks]
  C --> P1[Tesser creates the payment record,<br/>recording the authorization outcome]
  P1 --> C1{Authorized?}
  C1 -->|No| R[Funds are not sent;<br/>the step fails]
  C1 -->|Yes| D[Funds confirmed on-chain]
  B -->|Direct on-chain transfer| D
  D --> REC{Reconciles to a record from<br/>a Travel Rule flow?}
  REC -->|No| P2[Tesser creates the<br/>payment record]
  REC -->|Yes| U[The step moves to confirmed]
  P2 --> S[Sending account populates on<br/>the estimated overlay]
  U --> S
  S --> BAL[Receiving wallet is credited]
  BAL --> F[Wallet screening]
  F --> G[Participants and risk fields populate;<br/>payment.risk_updated fires]
  G --> H[Block finalizes:<br/>step.completed fires]`}
/>

:::note{title="A note on settlement"}
For inbound payments, funds are credited to your available balance **regardless of the screening outcome**. Tesser's `risk_status` is an advisory signal you consume to decide whether to credit your end user; it is not a hold on funds Tesser has already credited. See [Why Availability Isn't Gated on the Screening Outcome](#why-availability-isnt-gated-on-the-screening-outcome).
:::

## Continuous Monitoring of Wallets for Inbound Payments

You do not need to pre-register inbound payments in order to receive funds on-chain. Tesser monitors wallets for on-chain activity and when an inbound transaction is detected, will create a payment record, counterparties and accounts (if needed), and notify your integration via webhooks. Tesser will first become aware of an on-chain transaction when that transaction is confirmed in a block.

When funds arrive on-chain, Tesser looks up the transaction against the Travel Rule data-exchange provider. If it corresponds to an authorization or Travel Rule message, the transfer is reconciled to the payment record created from that flow rather than creating a second record.

## How the Sending Wallet Is Identified

Tesser identifies the participant(s) on the other side of an inbound payment in the payment's top-level `participants[]` array. For an inbound payment, the array can identify the `ORIGINATOR`, the `ORIGINATOR_VASP`, a `CUSTODY_SERVICE` and/or `INTERMEDIARY_VASP`, the `BENEFICIARY` (the counterparty or tenant that owns the receiving wallet), or a `CONTRACT` when the sending address is a contract rather than an end-user wallet.

Which roles populate, where each one's identity data comes from, and how each is reconciled to your records depends on whether Travel Rule data accompanied the transfer and whether the sending wallet is VASP-hosted or self-hosted. 

:::note
See [Payment Participants](/overviews/payment-participants) for the per-role tables, the `resource` object's fields, and the full set of `resolution` values.
:::

Four points matter most for an inbound integration:

- **Names are matched exactly.** Provider-supplied names are scored against your counterparty and tenant records; a record is matched only on an exact match. Where a non-beneficiary participant does not match, Tesser creates a new counterparty for it and the entry carries `resolution: "COUNTERPARTY_CREATED"`.
- **Your beneficiary record is canonical.** Tesser never creates a new beneficiary record from provider data. If an originator VASP supplies a beneficiary name that does not match your record, the `BENEFICIARY` entry carries `resolution: "DID_NOT_MATCH"`. An inbound payment with `resolution: "DID_NOT_MATCH"` for the `BENEFICIARY` role is how you know a Travel Rule authorization was received but rejected prior to funds movement. Where no provider-supplied beneficiary name existed to match at all, the entry carries `resolution: "USER_SUPPLIED_ID"`.
- **One sending wallet can produce more than one array entry.** A hot wallet used to send funds out of an exchange, for example, may be attributed both to the exchange itself (`ORIGINATOR_VASP`) and to the third-party custodian that holds its wallets (`CUSTODY_SERVICE`).
- **Travel Rule data takes precedence for entity attribution.** Where a Travel Rule authorization flow supplied the identity data, Tesser treats it as the source of truth for the roles it covers. Wallet screening attribution still fills in roles the Travel Rule payload does not describe, e.g. a custody service or an intermediary VASP the originator VASP did not declare.

### Recording the Sending Wallet on the Payment

When Tesser records an inbound payment from a wallet address it has not seen before, it automatically creates an unmanaged wallet account (`is_managed: false`) for the sending wallet and assigns it to the counterparty the sender reconciled to. If Tesser recognizes the wallet address — because the sender is a repeat sender, or because an outbound payment previously sent funds to this address — Tesser identifies the existing account by looking up the wallet address.

`estimated.from.account_id` populates as soon as Tesser has the transaction, because the sending address is on the transaction itself — it does not wait for the screening outcome. For a direct on-chain transfer it is present from record creation; for a Travel Rule flow it fills in when funds land on-chain. The `participants[]` entry that identifies *who* that wallet belongs to arrives later, with the screening result. See [When Each Field Populates](#when-each-field-populates).

The auto-created account follows this shape:

```json
{
  "id": "2c7a9e1f-3b8d-4625-a0f4-6d1e5c9b2a8f",
  "workspace_id": "b53f6690-3242-4942-9907-885779632832",
  "tenant_id": null,
  "counterparty_id": "b8b8fc6c-62b5-4617-bc45-28ac30456f49",
  "type": "stablecoin_ethereum",
  "name": "External Wallet 0x742d35Cc6634...95f8fE0B",
  "crypto_wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE0B",
  "is_managed": false,
  "assets": [
    {
      "currency": "USDC",
      "network": "ETHEREUM",
      "available_balance": "0"
    }
  ],
  "created_at": "2024-03-01T10:02:00.000Z",
  "updated_at": "2024-03-01T10:02:00.000Z"
}
```

The account is assigned to whichever record the sender reconciled to. Read the corresponding `participants[]` entry's `resource.resource_type` to know which: for a `COUNTERPARTY` the account carries `counterparty_id`, and looking that counterparty up returns its `tenant_id` for tenanted integrations; for a `TENANT` the sender resolved to one of your tenants directly; `WORKSPACE` means the funds came from one of your own treasury wallets. Subsequent inbound payments from the same wallet address reuse this account.

The sending account's `assets[].available_balance` stays `"0"`. Tesser does not monitor balances on unmanaged accounts — the record exists to identify where the funds came from, not to track what that wallet holds.

### Counterparty Names for Unattributed Sending Wallets

Where the sender cannot be identified by name (the transfer carried no Travel Rule data, and the wallet screening provider attributes the address to an unnamed wallet cluster rather than a named entity), Tesser still creates a counterparty so the payment has something to reconcile to. These counterparties are named after the cluster: the words "Wallet Cluster" followed by the full wallet address, for example `Wallet Cluster 0x742d35Cc6634C0532925a3b844Bc9e7595f8fE0B`.

A cluster groups the address(es) the screening provider believes are controlled by the same owner. A counterparty with a Wallet Cluster name therefore means "we know these addresses belong to one owner, but not the specific identity of that owner." Repeat sends from any address in the cluster reconcile to the same counterparty.

## When Each Field Populates

An inbound payment record is created at the earliest point Tesser has something to record, and its remaining fields fill in as the compliance checks resolve.

<div className="balanced-table field-timing-cols">

| Stage | What has happened | What populates |
| --- | --- | --- |
| **Travel Rule authorization request received** *(Travel Rule flows only)* | Tesser runs the beneficiary-name match and authorizes or rejects the request, then creates the payment record and its single `transfer` step in `created` status. Funds have not yet moved. | `estimated.to.account_id`, and the `estimated` amount, currency, and network at both the top level and the step level; `participants[]`; the `TRAVEL_RULE` entry in `provider_metadata[]`. `estimated.from.account_id` is left blank. `risk_status` is `unchecked` and `risk_status_reasons[]` is empty. |
| **Funds land on-chain** | For a Travel Rule record, Tesser reconciles the transfer to it and now knows which wallet sent the funds; the step moves from `created` to `confirmed`. When no record matches, Tesser creates one now, with its step already in `confirmed`. Either way, wallet screening kicks off and the receiving wallet is credited. | The step's `status`, `transaction_hash`, and `confirmed_at`; and `estimated.from.account_id`. The receiving wallet's available and total balance are credited. For records created at this stage: `estimated.to.account_id`, network, and estimated amounts are also populated, with `participants[]` empty, `risk_status` `unchecked`, and `risk_status_reasons[]` empty. |
| **Screening resolves** | The wallet screen completes and Tesser determines whether the sender's identity reconciles to existing records or requires new counterparties. | `risk_status` and `risk_status_reasons[]`; the `WALLET_RISK_SCREENING` entry in `provider_metadata[]`; and, for records created at the funds-landing stage, `participants[]`, all at once. |
| **Block finalizes** | The transaction is final and can no longer be reorganized away. | The step moves to `completed`; the step-level and top-level `actual` overlays populate. The balance does not change here — it was credited at confirmation, and finalization makes that credit permanent. |
| **Manual review** *(only when `risk_status` is `awaiting_decision`)* | Your reviewer records a decision. | `risk_status` moves to `manually_approved` or `manually_rejected`; `risk_reviewed_by` and `risk_reviewed_at` populate. `risk_status_reasons[]` is unchanged from the automatic outcome. |

</div>

`participants[]` is populated once in a single write. For the timing across all payment directions, see [When participants[] Populates](/overviews/payment-participants#when-participants-populates).


## Inbound Payment Creation (`payment.created`)

`payment.created` fires when Tesser creates the payment record. When that happens depends on the entry path.

:::note{title="About the payload examples on this page"}
The webhook payloads below are **abridged** to the fields that matter at each stage — `// ...` marks fields present on the wire but omitted here. Every payload carries the complete payment resource, wrapped in the standard `{ id, type, created_at, data: { object } }` envelope. For the full field list see the [Payments API reference](/api/payments); for the envelope, see [Webhooks](/webhooks/general).
:::

### Travel Rule Authorization Flow

For a payment that begins with a Travel Rule authorization request, `payment.created` fires **before funds move**. The payload records the destination account, the reconciled participants, the raw transfer record in `provider_metadata[]`, and the single `transfer` step in `created` status — the step exists to hold the money movement the authorization anticipates, but nothing has happened on-chain yet, so `transaction_hash` and `confirmed_at` are both `null`.

The sending account is necessarily absent at this point because the transaction has not happened yet:

- **`estimated.from.account_id` is `null`.** The originator VASP tells Tesser who is sending, not which wallet they will send from, so there is no sending account to reference until funds land. It is never pre-filled with a stand-in.
- **`estimated.*.network` is populated.** Tesser only authorizes supported CAIP-19 assets, so the declared token identifies the network before funds move.

`expires_at` is `null`, as it is on every inbound payment.

#### Beneficiary Name Match
Once Tesser receives the Travel Rule data from the Originator VASP, it will check whether the Beneficiary name sent by the Originator matches the name your organization recorded for the counterparty owner of the destination wallet account. In other words, does your organization and the Originator VASP agree that "John Smith" owns the destination wallet, or does the Originator VASP think a different entity owns the destination wallet?

If the check passes, Tesser will return an "authorization" response to the Travel Rule data provider. If the check fails, Tesser will instruct the Travel Rule provider to reject the inbound payment.

The result of the check is visible on the `participants[]`. The `BENEFICIARY` entry in `participants[]` will be `EXACT_MATCH` for an authorized request, `DID_NOT_MATCH` for one Tesser rejected on the beneficiary-name match.

```json
{
  "type": "payment.created",
  "data": {
    "object": {
      "id": "9f3b7e1c-4a2d-4f69-b5a8-0c6e2d9f1a3b",
      "direction": "inbound",
      "estimated": {
        "from": { "account_id": null, "amount": "1000.00", "currency": "USDC", "network": "BASE" },
        "to": { "account_id": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b", "amount": "1000.00", "currency": "USDC", "network": "BASE" }
      },
      "risk_status": "unchecked",
      "risk_status_reasons": [],
      "participants": [
        {
          "role": "ORIGINATOR",
          "resource": {
            "resource_type": "COUNTERPARTY",
            "id": "3c9a1e57-8b2f-4d06-9e41-7a5c2f8d1b60",
            "name_match_score": "23",
            "name_match_threshold": "99",
            "name_match_comparison_id": "b41f7c29-6d83-45ea-a1c7-9f20d3e8b574",
            "resolution": "COUNTERPARTY_CREATED"
          }
        }
        // ... ORIGINATOR_VASP and BENEFICIARY entries
      ],
      "provider_metadata": [
        {
          "provider_metadata_type": "TRAVEL_RULE",
          "provider_metadata_key": "NOTABENE",
          "provider_metadata_data": {
            // ... the raw transfer record, passed through exactly as the originator VASP sent it
          }
        }
      ],
      "expires_at": null,
      "steps": [
        {
          "step_sequence": 1,
          "step_type": "transfer",
          "status": "created",
          "transaction_hash": null,
          "confirmed_at": null
          // ...
        }
      ]
      // ...
    }
  }
}
```

When funds subsequently land on-chain, Tesser matches the on-chain transaction to the payment record and moves the step from `created` to `confirmed` status. That transition fires `step.confirmed` followed by `payment.updated` webhooks, which carry the step's new status, `transaction_hash`, `confirmed_at`, and the now-known `estimated.from.account_id`. From that point the payment follows the same path as a direct on-chain transfer.

`step.confirmed` is specific to this entry path. A direct on-chain transfer never fires it, because its step is already `confirmed` when the record is created — there is no transition to report.

### Direct On-Chain Transfer

For a transfer that arrives with no Travel Rule data, `payment.created` fires when Tesser sees the transaction in a block. The step is already in `confirmed` and the `estimated` overlay is fully populated at both the top level and the step level, `estimated.from.account_id` included — the sending address is on the transaction, so the sending account is known from the outset. `participants[]` and `provider_metadata[]` are empty, because both depend on the screening result, and the `actual` overlay is present with all sub-fields still `null`.

```json
{
  "type": "payment.created",
  "data": {
    "object": {
      "id": "9f3b7e1c-4a2d-4f69-b5a8-0c6e2d9f1a3b",
      "direction": "inbound",
      "estimated": {
        "from": { "account_id": "2c7a9e1f-3b8d-4625-a0f4-6d1e5c9b2a8f", "amount": "1000.00", "currency": "USDC", "network": "ETHEREUM" },
        "to": { "account_id": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b", "amount": "1000.00", "currency": "USDC", "network": "ETHEREUM" }
      },
      "actual": {
        "from": { "account_id": null, "amount": null, "currency": null, "network": null },
        "to": { "account_id": null, "amount": null, "currency": null, "network": null }
      },
      "risk_status": "unchecked",
      "risk_status_reasons": [],
      "participants": [],
      "provider_metadata": [],
      "steps": [
        {
          "step_sequence": 1,
          "step_type": "transfer",
          "status": "confirmed",
          "transaction_hash": "0xabcdef0123",
          "confirmed_at": "2024-03-01T10:02:00.000Z",
          "completed_at": null
          // ...
        }
      ]
      // ...
    }
  }
}
```

## Inbound Payment Risk Review (`payment.risk_updated`)

Tesser screens the sending wallet against your organization's risk policy and reports the outcome on the payment as `risk_status` via a `payment.risk_updated` webhook. Screening runs on every inbound payment, whether or not a Travel Rule authorization flow preceded it. See [Wallet Risk Screening Data](/overviews/wallet-risk-screening) for how to read `risk_status_reasons[]`, and [Wallet Screening Risk Categories](/overviews/wallet-screening-risk-categories) for the category values.

For a payment that arrived as a direct on-chain transfer, the `payment.risk_updated` payload is also where `participants[]` first populates: identifying who the sending wallet belongs to depends on the screening attribution, so the two land together. The sending *account* is not part of this — `estimated.from.account_id` was already populated when Tesser recorded the transaction.

Risk screening runs in parallel with on-chain finalization, so `payment.risk_updated` and `step.completed` can fire in either order.

If your policy requires manual review, an initial `payment.risk_updated` fires with `risk_status: "awaiting_decision"`. Optionally submit a decision via the [risk review decision API](/api/payments#submit-risk-review) or in the Tesser dashboard. If a decision is recorded, `risk_reviewed_by` and `risk_reviewed_at` are populated and a second `payment.risk_updated` fires with `risk_status` transitioned to `manually_approved` or `manually_rejected`.

<Tabs>
  <TabItem label="Automatically approved">

Example webhook payload when the sending wallet is automatically approved per your organization's policy. The sending wallet was attributed to a VASP whose name did not exactly match an existing counterparty, so a new counterparty was created for it; the beneficiary carries `USER_SUPPLIED_ID` because no Travel Rule data accompanied the transfer and there was no supplied name to match:

```json
{
  "type": "payment.risk_updated",
  "data": {
    "object": {
      "id": "9f3b7e1c-4a2d-4f69-b5a8-0c6e2d9f1a3b",
      "estimated": {
        "from": { "account_id": "2c7a9e1f-3b8d-4625-a0f4-6d1e5c9b2a8f", "amount": "1000.00", "currency": "USDC", "network": "ETHEREUM" },
        "to": { "account_id": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b", "amount": "1000.00", "currency": "USDC", "network": "ETHEREUM" }
      },
      "risk_status": "automatically_approved",
      "risk_status_reasons": [],
      "participants": [
        {
          "role": "ORIGINATOR_VASP",
          "resource": {
            "resource_type": "COUNTERPARTY",
            "id": "b8b8fc6c-62b5-4617-bc45-28ac30456f49",
            "name_match_score": "89",
            "name_match_threshold": "99",
            "name_match_comparison_id": "f6a2c81d-3e97-4b5a-a24e-8c1d0b9e6f42",
            "resolution": "COUNTERPARTY_CREATED"
          }
        },
        {
          "role": "BENEFICIARY",
          "resource": {
            "resource_type": "COUNTERPARTY",
            "id": "e8b3d016-4f7a-49c2-95d8-2c61a0f9e735",
            "name_match_score": null,
            "name_match_threshold": null,
            "name_match_comparison_id": null,
            "resolution": "USER_SUPPLIED_ID"
          }
        }
      ],
      "provider_metadata": [
        {
          "provider_metadata_type": "WALLET_RISK_SCREENING",
          "provider_metadata_key": "TRM_LABS",
          "provider_metadata_data": {
            // ... the raw screening response
          }
        }
      ]
      // ...
    }
  }
}
```

  </TabItem>
  <TabItem label="Manually approved">

Example webhook payload after a reviewer manually approves the sending wallet. The screen surfaced a High-severity counterparty exposure above its materiality threshold, which routed the payment to review:

```json
{
  "type": "payment.risk_updated",
  "data": {
    "object": {
      "id": "9f3b7e1c-4a2d-4f69-b5a8-0c6e2d9f1a3b",
      "risk_status": "manually_approved",
      "risk_status_reasons": [
        {
          "risk_status_reason_type": "COUNTERPARTY",
          "risk_status_reason_category": "Mixer",
          "risk_status_reason_message": "Address has COUNTERPARTY exposure to Mixer",
          "risk_status_reason_severity": "High",
          "risk_status_reason_significance": "DECISIVE",
          "risk_status_reason_participant_id": null,
          "volume_usd_percent": "38.43",
          "volume_usd_percent_threshold": "2",
          "volume_usd_amount": "1225310.31",
          "volume_usd_amount_threshold": null,
          "confidence_score": null,
          "confidence_score_threshold": null
        }
      ],
      "risk_reviewed_by": "8e1f5a3b-2c4d-4e6f-9a8b-7d3c5e9f1a2b",
      "risk_reviewed_at": "2024-03-01T10:05:30.000Z"
      // ...
    }
  }
}
```

  </TabItem>
</Tabs>

## Inbound Payment Completion (`step.completed` and `payment.updated`)

When the block finalizes, several things occur:
- The step status transitions from `confirmed` to `completed`.
- The step-level and top-level `actual` overlays populate.
- Tesser fires `step.completed` followed by `payment.updated` webhooks.

Finalization does not move the balance. The receiving wallet was credited earlier, when the transaction was confirmed in a block — see [Spending the Inbound Funds](#spending-the-inbound-funds). What finalization changes is that the credit can no longer be reversed.

The `payment.updated` payload below shows a terminal state in which risk was manually approved before the block finalized. Because screening and finalization are independent, `step.completed` can fire first — in that case this payload's `risk_status` will still be `"unchecked"` or `"awaiting_decision"`, and `participants[]` will still be empty for a direct on-chain transfer. The `actual` overlay is unaffected either way: it mirrors the settled transaction, including `actual.from.account_id`, and does not wait on the screen.

```json
{
  "type": "payment.updated",
  "data": {
    "object": {
      "id": "9f3b7e1c-4a2d-4f69-b5a8-0c6e2d9f1a3b",
      "actual": {
        "from": { "account_id": "2c7a9e1f-3b8d-4625-a0f4-6d1e5c9b2a8f", "amount": "1000.00", "currency": "USDC", "network": "ETHEREUM" },
        "to": { "account_id": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b", "amount": "1000.00", "currency": "USDC", "network": "ETHEREUM" }
      },
      "risk_status": "manually_approved",
      "steps": [
        {
          "step_sequence": 1,
          "status": "completed",
          "transaction_hash": "0xabcdef0123",
          "confirmed_at": "2024-03-01T10:02:00.000Z",
          "completed_at": "2024-03-01T10:14:00.000Z",
          "failed_at": null
          // ...
        }
      ]
      // ...
    }
  }
}
```

## Spending the Inbound Funds

The receiving wallet's available and total balance are credited when the step reaches `confirmed` — that is, as soon as the transaction is in a block, not when the block finalizes.

**We nevertheless recommend waiting for the step to reach `completed` before spending inbound funds.** Do not attempt on-chain transfers, rebalances, payouts, or off-ramps to fiat against a credit that is only `confirmed`. Until the block finalizes the transaction can still be reorganized away, and if that happens Tesser reverses the credit — leaving you having spent against funds that no longer exist. Finalization is what makes the credit permanent.

The credit lands on the receiving managed wallet's matching asset — the entry whose `currency` and `network` match the payment. Fetch it with `GET /v1/accounts/{estimated.to.account_id}`:

```json
{
  "id": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b",
  "workspace_id": "b53f6690-3242-4942-9907-885779632832",
  "tenant_id": null,
  "counterparty_id": "e8b3d016-4f7a-49c2-95d8-2c61a0f9e735",
  "type": "stablecoin_ethereum",
  "name": "Jordan Rivera USDC Wallet",
  "crypto_wallet_address": "0x9f2c4e8a1b6d3f507c9e2a4b8d1f6039e5c7a2b4",
  "is_managed": true,
  "assets": [
    {
      "currency": "USDC",
      "network": "ETHEREUM",
      "available_balance": "1000.00"
    }
  ],
  "created_at": "2024-02-14T08:30:00.000Z",
  "updated_at": "2024-03-01T10:14:00.000Z"
}
```

Balances are returned in major units, the same convention the payment's `estimated` and `actual` amounts use, so a balance and a payment amount are directly comparable.

The balance is credited regardless of the risk outcome — risk screening does not hold the funds. We recommend, separately from the finality point above, that you wait for an approving `risk_status` (`automatically_approved` or `manually_approved`) before spending inbound funds, and reconcile any `automatically_rejected` or `manually_rejected` outcome against your own compliance policy before putting the funds to use.

### Why Availability Isn't Gated on the Screening Outcome

The credit tracks the chain, not the screening outcome. Tesser credits the wallet when the transaction lands and reverses that credit only if the chain itself takes the transaction back; `risk_status` never holds funds. That is a deliberate choice: if Tesser withheld a rejected payment from available balance, the funds would be stranded in the receiving wallet with no way to act on them, because an on-chain transfer cannot be sent back automatically. Crediting the balance preserves your ability to respond to a rejection — returning the funds to the originator, say, or moving them to a dedicated wallet to quarantine them. Your organization's compliance policy should determine what to do with the funds after rejection.

:::warning{title="Do not naively return funds to the sender's address"}
The sending wallet may not be a self-hosted wallet under the original sender's control. It could be an exchange hot wallet, a smart-contract address, or any other on-chain address — the `participants[]` array will tell you when the sender was attributed to a hosting VASP (`ORIGINATOR_VASP`) or to a contract (`CONTRACT`). Returning funds to such an address could deposit them into a pooled exchange account or a contract from which they cannot be recovered. If you need to return funds, coordinate the return address with the sender out of band rather than reusing the sender account's `crypto_wallet_address` (retrievable by looking up `estimated.from.account_id` via `GET /v1/accounts/{account_id}` — use `estimated`, not `actual`, since you may be acting on a rejection before the block finalizes).
:::

## Failure Modes for Inbound Payments

An inbound payment fails in one of two ways, and both look the same on the wire: the step transitions to `failed`, `step.failed` fires, `payment.updated` follows with the failed step, and the funds do not remain in the receiving wallet. What distinguishes them is how far the payment got. A chain reorganization fails a step that had reached `confirmed` — there was a transaction, it was credited, and both were undone. A rejected authorization or a sender who never broadcasts fails a step still in `created` — no transaction ever existed and nothing was ever credited. Read the step's prior status and its `status_reasons` to tell which occurred.

### Chain Reorganization

If the block containing the on-chain transaction does not finalize — most commonly because a chain reorganization removes the transaction from the canonical chain — the inbound payment terminates and the credit made at confirmation is reversed. This is the case the "wait for `completed`" recommendation exists for: the funds were in the wallet's available balance, and then they were not.

What you will observe:

- `step.failed` fires. The step transitions from `confirmed` to `failed`, `failed_at` populates, and step-level `status_reasons` carries the failure detail.
- `payment.updated` fires alongside, carrying the full updated payment with the failed step.
- Top-level `actual.*` stays all-null because nothing settled. `estimated.*` remains populated from record creation, preserving the original observation; `desired.*` is all-null as always for inbound.
- **Risk screening is unaffected.** A reorganization fails the step; it does not cancel a screen in progress or suppress its result. A screen that was still running completes normally and `payment.risk_updated` fires as usual, and a screen that had already completed keeps its `risk_status`, `risk_status_reasons[]`, and `WALLET_RISK_SCREENING` entry in `provider_metadata[]` on the failed payment. The screening outcome describes the sending wallet, which does not become less true because the transaction was undone. For a payment that came through a Travel Rule authorization flow, `participants[]` and the `TRAVEL_RULE` entry in `provider_metadata[]` were written at record creation and are likewise preserved.
- The credit is reversed. The receiving wallet's available and total balance return to what they were before the transaction was confirmed.

Example `payment.updated` webhook payload at the terminal failed state, for a payment that arrived as a direct on-chain transfer whose screening had not yet completed when the reorganization was detected. `risk_status` is `unchecked` and `participants[]` is empty *at this moment only* — the screen is still running, and a later `payment.risk_updated` will carry its outcome:

```json
{
  "type": "payment.updated",
  "data": {
    "object": {
      "id": "9f3b7e1c-4a2d-4f69-b5a8-0c6e2d9f1a3b",
      "actual": {
        "from": { "account_id": null, "amount": null, "currency": null, "network": null },
        "to": { "account_id": null, "amount": null, "currency": null, "network": null }
      },
      "risk_status": "unchecked",
      "risk_status_reasons": [],
      "participants": [],
      "steps": [
        {
          "step_sequence": 1,
          "status": "failed",
          "status_reasons": [
            {
              "error_code": "transfers-9202",
              "error_message": "The inbound transaction was reorganized away before finality"
            }
          ],
          "transaction_hash": "0xabcdef0123",
          "completed_at": null,
          "failed_at": "2024-03-01T10:14:00.000Z"
          // ...
        }
      ]
      // ...
    }
  }
}
```

### Rejected or Never-Funded Travel Rule Records

A payment record created from a Travel Rule authorization request that Tesser rejected on the beneficiary-name match — or one whose funds simply never arrive — records the authorization outcome and the supplied identity data, but no money ever moves against it. The step fails rather than lingering: this is a terminal state, reached without any on-chain transaction.

- **The step transitions from `created` to `failed`,** `failed_at` populates, and step-level `status_reasons` carries the reason. `step.failed` fires, followed by `payment.updated`. Note the step never reaches `confirmed` — that is what separates this from a chain reorganization, where the step was `confirmed` before it failed.
- `estimated.from.account_id`, the step's `transaction_hash`, `confirmed_at`, and every `actual.*` field stay `null`; there was never an on-chain transaction to reconcile. The declared amount, currency, network, and destination remain on `estimated`.
- `participants[]` and the `TRAVEL_RULE` entry in `provider_metadata[]` are preserved as written at record creation. A rejected authorization is identifiable from the `BENEFICIARY` entry's `resolution: "DID_NOT_MATCH"`.
- `risk_status` remains `unchecked` — wallet screening runs against the sending wallet, which is only known once funds land, so it never runs for a record that is never funded. This is the one case where `unchecked` is terminal rather than transitional: the step reaching `failed` is your signal that no screening outcome is coming.
- The receiving wallet's balance is untouched. Nothing was ever credited, because the credit happens at `confirmed` and this step never got there.

## Inbound Payment Event Sequence

The happy path for an inbound payment fires the following webhook events. `payment.risk_updated` and `step.completed` run on independent clocks and can fire in either order; the receiving wallet's balance is credited at `confirmed`, before either of them, regardless of the risk outcome.

| Event | Fires when |
| --- | --- |
| `payment.created` | Tesser creates the payment record, always with one `transfer` step and the `estimated` overlay populated at the top level and the step level. **Travel Rule flow:** fires when the authorization request is handled, before funds move; the step is in `created`, `estimated.from.account_id` is `null`, and the declared amount, currency, network, destination, `participants[]`, and `TRAVEL_RULE` metadata are present. **Direct on-chain transfer:** fires when the transaction is included in a block; the step is already in `confirmed`, `estimated.from.account_id` is populated, and `participants[]` and `provider_metadata[]` are still empty. |
| `step.confirmed` | *(Travel Rule flow only.)* Funds land on-chain and Tesser matches the transaction to the record created from the authorization request, moving the step from `created` to `confirmed`. A direct on-chain transfer does not fire this event — its step is already `confirmed` at `payment.created`, so there is no transition. |
| `payment.updated` | *(Travel Rule flow only.)* Fires alongside `step.confirmed`, carrying the step's new status, `transaction_hash`, `confirmed_at`, and the now-known `estimated.from.account_id`. |
| `payment.risk_updated` | The risk screening of the sending wallet completes. `risk_status` transitions to `automatically_approved`, `automatically_rejected`, or `awaiting_decision`. For a direct on-chain transfer the same payload carries the newly populated `participants[]` and the `WALLET_RISK_SCREENING` entry in `provider_metadata[]`. For the manual-review path, a second `payment.risk_updated` fires once a reviewer records a decision via the [risk review decision API](/api/payments#submit-risk-review) or in the Tesser dashboard, transitioning `risk_status` to `manually_approved` or `manually_rejected` and populating `risk_reviewed_by` and `risk_reviewed_at`. |
| `step.completed` | The block finalizes. The step transitions to `completed` and the step-level `actual` overlay populates. |
| `payment.updated` | Fires alongside `step.completed`. The top-level `actual` overlay populates to reflect the finalized amounts. For a Travel Rule flow this is the second `payment.updated` on the payment. |

The step's status is the shortest summary of where a payment stands: `created` (recorded, nothing on-chain yet) → `confirmed` (in a block) → `completed` (finalized), or `failed` from either of the first two. Every failure path shares one shape — `step.failed` followed by `payment.updated`, with the payment terminating without crediting funds — whether the cause is a chain reorganization, a rejected Travel Rule authorization, or a sender who never broadcasts. See [Failure Modes for Inbound Payments](#failure-modes-for-inbound-payments) for what distinguishes them on the record.

## Reconciliation via the API

If your integration misses a webhook or needs to reconcile state on a schedule, fetch inbound payments via the Payments API:

```bash
GET /v1/payments?direction=inbound
```

Auto-created unmanaged wallet accounts are also retrievable via `GET /v1/accounts` with the `is_managed=false` filter, and the counterparties they belong to via `GET /v1/counterparties`. See the [Payments API reference](/api/payments), [Accounts API reference](/api/accounts), and [Counterparties API reference](/api/counterparties) for the full set of query parameters.
