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 for the shared data shape, lifecycle phases, and status taxonomy that apply to inbound payments along with Tesser's other funds-movement resources.
What's Different About Inbound Payments
Inbound payments share the Payment resource shape with outbound payments but diverge in several lifecycle and data-model ways. Note these before you begin your integration:
- No customer-side planning. The
desiredoverlay does not populate.estimatedpopulates when Tesser records the inbound on-chain transaction (atpayment.created);actualpopulates only when the block finalizes (atstep.completed/payment.updated). There is no quote, nopayment.quote_createdwebhook, and noPATCHstep. - No balance check.
balance_statusis set tounreservedwhen the payment record is created and never updates. The funds have already arrived on-chain, so there is nothing to reserve. - 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.
- One step. The payment contains exactly one step, of type
transfer, born inconfirmedstate when the payment record is created. The step transitions tocompletedwhen the block finalizes. - Placeholder counterparty for senders. Each sending account maps to the same, single placeholder counterparty record. (If your integration uses tenants, there will be a placeholder counterparty per tenant). Tesser cannot identify who owns the sending wallet, so all senders of inbound payments share the same placeholder counterparty.
- Risk screening targets the sender. 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 on-chain finalization and does not gate the credit — the receiving wallet's available balance updates when the block finalizes, regardless of the risk outcome.
Prerequisites
Before your integration can receive inbound payments:
- At least one managed wallet at Tesser. Inbound payments are credited to a managed wallet that Tesser monitors on-chain. Provision via Create an Account.
- A placeholder counterparty. Your workspace is provisioned with a placeholder counterparty (per tenant, if applicable) the first time you receive an inbound payment. No customer action is required — Tesser sets this up for you.
- Webhooks for inbound-payment events. Ensure you're listening to
payment.created,payment.risk_updated,step.completed, andpayment.updated. See Webhook Authentication for setup.
Continuous Monitoring 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 and account (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.
Sending Wallet Account Handling
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 placeholder counterparty (scoped to the receiving managed wallet's tenant, if applicable).
If Tesser recognizes the wallet address (either because the sender is a repeat sender, or because an outbound payment previously sent funds to this wallet address), then Tesser will identify the account identifier by looking up the wallet address.
The auto-created account follows this shape:
Code
The counterparty_id points to the placeholder counterparty for this sender, which has a recognizable 00000000- prefixed UUID. To determine which tenant the sender belongs to (for tenanted integrations), look up the counterparty by its counterparty_id — the counterparty record returns the tenant_id. The counterparty name for customers without tenants will be Inbound Payments Counterparty. For customers using tenants in their integrations, the counterparty name will take the form Inbound Payments Counterparty - {Tenant Name}. Subsequent inbound payments from the same wallet address reuse this account.
Inbound Payment Creation (payment.created)
When Tesser becomes aware of an on-chain transaction from a sending wallet, Tesser creates an inbound payment to record that transaction and fires payment.created. The payload contains the full payment resource with the estimated overlay populated at both the top level and the step level. The actual overlay is present but its sub-fields are still null.
The estimated.from.account_id references the unmanaged wallet account for the sender. The account is retrievable via GET /v1/accounts after payment.created arrives.
Code
Inbound Payment Risk Review (payment.risk_updated)
After payment.created, Tesser screens the sending wallet (represented by estimated.from.account_id) against your organization's risk policy. The outcome is reported on the payment as risk_status via a payment.risk_updated webhook. See Risk Statuses on the lifecycle overview for the full taxonomy.
Risk screening runs in parallel with on-chain finalization, so payment.risk_updated and step.completed can fire in either order. Screening does not gate the balance credit — the receiving wallet's available balance updates when the block finalizes (step.completed), regardless of the risk outcome. The screening result notifies you so you can act on it under your own compliance policy.
If your policy requires manual review, an initial payment.risk_updated fires with risk_status: "awaiting_decision". Submit a decision via the risk review decision API or in the Tesser dashboard. After 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.
Example webhook payload when the sending wallet is auto-approved per your organization's policy:
Code
Inbound Payment Completion (step.completed and payment.updated)
When the block finalizes, several things occur:
- The step status transitions from
confirmedtocompleted. - The step-level and top-level
actualoverlays populate. - Tesser fires
step.completedfollowed bypayment.updatedwebhooks. - Tesser updates the receiving wallet's available balance. The credit happens at finalization regardless of the risk outcome — risk screening does not gate it.
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" instead.
Code
Spending the Inbound Funds
Inbound funds become spendable once the step reaches completed. The receiving wallet's available balance does not update until then, and you should not attempt on-chain transfers, rebalances, payouts, or off-ramps to fiat before the step completes. Until the step reaches completed, the on-chain transaction is in a block but the block has not yet finalized, so the transaction can still be reorganized away.
Available balance is credited at finalization regardless of the risk outcome — risk screening does not hold the funds. We recommend, however, that you wait for an approving risk_status (auto_approved or manually_approved) before spending inbound funds, and reconcile any auto_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
On-chain transfers are irreversible — once the block finalizes, the funds have settled in your wallet and cannot be sent back automatically. Tesser credits the available balance regardless of the risk outcome so that you retain control of those funds. If Tesser instead withheld a rejected payment from available balance, the funds would be stranded in the receiving wallet with no way to act on them. Crediting the balance preserves your ability to respond to a rejection — for example, returning the funds to the originator or moving them to a dedicated wallet to quarantine the funds. Your organization's compliance policy should determine what to do with the funds after rejection.
Do not naively return funds to the sender's address
The sending wallet may not be a self-custodial wallet under the original sender's control. It could be an exchange hot wallet, a smart-contract address, or any other on-chain address. Returning funds to it 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 actual.from.account_id via GET /v1/accounts/{account_id}).
Failure Modes for Inbound Payments
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 without crediting funds.
What you will observe:
step.failedfires. The step transitions fromconfirmedtofailed,failed_atpopulates, and step-levelstatus_reasonscarries the failure detail.payment.updatedfires alongside, carrying the full updated payment with the failed step.- Top-level
actual.*stays all-null because nothing settled.estimated.*remains populated frompayment.created, preserving the original observation;desired.*is all-null as always for inbound. - Risk check process will terminate. If the risk check had not commenced, that process will not begin (
risk_statusremainsunchecked). If the risk check had completed, but the webhook (payment.risk_updated) had not already fired by the time the reorg is detected, it will not fire. The outcome of the risk check will remain available via API. - The receiving wallet's available balance is not updated — no funds were credited.
Example payment.updated webhook payload at the terminal failed state (risk had not yet completed when the reorg was detected):
Code
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 available balance updates when step.completed fires, regardless of the risk outcome.
| Event | Fires when |
|---|---|
payment.created | The on-chain transaction is included in a block. Tesser records the inbound payment with one step in confirmed state and populates the estimated overlay at the top level and at the step level. The actual overlay does not populate yet. |
payment.risk_updated | The risk screening of the sending wallet completes. risk_status transitions to auto_approved, auto_rejected, or awaiting_decision. For the manual-review path, a second payment.risk_updated fires once a reviewer records a decision via the risk review decision API 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. |
If the on-chain transaction does not finalize (chain reorganization), the happy path is replaced by step.failed followed by payment.updated and the inbound payment terminates without crediting funds. See Failure Modes for Inbound Payments for details.
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:
Code
Auto-created unmanaged wallet accounts are also retrievable via GET /v1/accounts with the is_managed=false filter. See the Payments API reference and Accounts API reference for the full set of query parameters.