This guide covers the wallet-step signing flow that applies whenever desired.from.account_id is a self-custodial wallet provisioned by Tesser, regardless of resource type (payment, rebalance, withdrawal). Tesser orchestrates the funds-movement lifecycle and prepares the unsigned transaction, but cannot submit the on-chain transaction itself — you must sign client-side with your wallet's signing key, then return the signature to Tesser via the resource-appropriate sign endpoint. Deposits via a liquidity provider — even those that land in a self-custodial wallet — don't require customer-signed steps, as the wallet is a destination for the deposit, not a source of funds. The wallet-step signing flow covered here is for resources where the customer's wallet is the source of funds.
Gas for on-chain transfers initiated through this flow is sponsored by Tesser. You do not need to fund the source wallet with native gas tokens — only the asset being transferred needs to be present on the source wallet.
The shape of the signing flow — the SDK call, the request body, the validation error codes, and the on-chain follow-up — is the same across all three resource types. Only the URL of the sign endpoint differs by resource.
Step Status Flow
A wallet-source step transitions through the following states:
| Status | Webhook event | Meaning |
|---|---|---|
created | (none) | The step has been planned and inserted into step_sequence but is not yet ready to sign. |
signature_requested | step.signature_requested | Tesser has prepared the unsigned transaction; the step DTO now carries unsigned_transaction. You sign client-side and submit the signature to the sign endpoint. |
signed | step.signed | The sign endpoint accepted your signature; Tesser begins broadcasting the transaction on-chain. |
submitted | step.submitted | Tesser has broadcast the signed transaction to the network. For Turnkey-managed wallets (the self-custodial wallet provider in scope for this flow), step.submitted is an event-only marker — the persisted step row transitions directly from signed to confirmed once the broadcast and chain confirmation are observed together, while the webhook payload and GET response both surface status: "submitted" and a populated submitted_at at the event boundary. |
confirmed | step.confirmed | The on-chain transaction is confirmed at the chain's required depth. transaction_hash is now populated on the step. |
completed | step.completed | Funds have settled at the destination. The step is terminal. |
The unsigned_transaction Field
When a wallet-source step reaches signature_requested, Tesser publishes the unsigned transaction on the step DTO as the unsigned_transaction field. The value is the serialized transaction call, ready to sign with the source wallet's key. The same field is retrievable at any time via a GET request on the parent resource: `GET /v1/payments/{paymentId}`, `GET /v1/treasury/rebalances/{rebalanceId}`, or `GET /v1/treasury/withdrawals/{withdrawalId}`.
Example field shape (truncated for display — the actual value is much longer):
Code
Sign the Step with the LocalSigner SDK
Use the LocalSigner from @tesser-payments/sdk to sign the step locally. The signature is returned as a string, which you then submit to the resource-appropriate sign endpoint (see the per-resource URL table in the next section).
Wallet and recipient addresses are automatically resolved from the parent resource's account IDs; you only need to pass the step object to signStep.
Code
The signStep method returns:
signature— send this to the sign endpoint to execute the step.unsignedTransaction— the serialized unsigned transaction (matchesunsigned_transactionon the step DTO).metadata— full signature details (stampHeaderName,stampHeaderValue,body). These stay client-side; onlysignatureis sent to Tesser.
Sign-Endpoint Contract
The sign endpoint takes a single JSON body with one field — the signature returned by LocalSigner.signStep:
Code
The body shape is identical across all three resources. Only the endpoint URL differs:
| Resource | Endpoint |
|---|---|
| Payment | POST /v1/payments/{paymentId}/steps/{stepId}/sign |
| Rebalance | POST /v1/treasury/rebalances/{rebalanceId}/steps/{stepId}/sign |
| Withdrawal | POST /v1/treasury/withdrawals/{withdrawalId}/steps/{stepId}/sign |
Use the URL that matches the parent resource — i.e., the resource that emitted the step.signature_requested webhook. The per-resource how-tos (Send a Payout (from a wallet), Rebalance Funds, Withdraw Funds via a Liquidity Provider) carry the surrounding scenario context for when wallet-step signing applies in each flow.
Sign-Endpoint Validation Error Codes
The sign endpoint validates the submitted signature before accepting it. Validation failures are surfaced as 4XX responses with an error_code identifying the cause. The rebalance and withdrawal sign endpoints share the treasury-30xx namespace; the payment sign endpoint uses payments-30xx.
| Semantic | Rebalance + Withdrawal | Payment |
|---|---|---|
| Invalid signature | treasury-3015 | payments-3013 |
| Signed transaction does not match step/payment | treasury-3016 | payments-3014 |
| Step not signable (wrong state) | treasury-3013 | — (no direct equivalent — see Errors overview) |
| Unsigned transaction missing | treasury-3014 | — (no direct equivalent — see Errors overview) |
Two of the four sign-validation semantics have direct counterparts across both code families; the other two — "step not signable" and "unsigned transaction missing" — are only surfaced as distinct codes by the treasury endpoints today. For the payment sign endpoint, those conditions are surfaced by adjacent codes in the broader payments-30xx range (for example, payments-3026 "Transfer step is already in status <status>. Cannot execute."). See the full Errors overview for the per-domain reference.
Sign-Endpoint Failure Response
A validation failure returns a 4XX response in the standard Tesser error envelope — a top-level errors array of error objects, each carrying an error_code from the table above. Example using treasury-3015 (invalid signature — the most commonly reproducible failure during integration testing):
Code
The equivalent response from the payment sign endpoint uses error_code: "payments-3013" ("signature is malformed or signed with incorrect key"). The envelope shape is the same; only the resource-domain prefix on the code differs.
After a validation failure, the step's status remains signature_requested and the step-level actual.* overlay remains all-null — the failure is at the sign endpoint, not at the step. Retry the signing flow with a fresh signature. For treasury-3013 ("step not signable"), the step may have advanced beyond signature_requested while you were preparing the signature — fetch the latest step state via GET on the parent resource before retrying.
Wallet-Source Insufficient Funds at Sign Time
The sign endpoint also validates that the source wallet has sufficient funds to cover desired.from.amount before accepting the signature. The balance check is synchronous: it runs inside the same request that submits the signature.
The full sequence:
- Sufficient funds. The sign endpoint accepts the signature, emits
step.signed, and emits<resource>.balance_updatedwithbalance_status: "reserved"from the sign-API path. - Insufficient funds. The sign endpoint returns a 4XX response and emits
<resource>.balance_updatedwithbalance_status: "awaiting_funds". The signature is not consumed; the step stays atsignature_requested. - On-chain funding watcher. Tesser monitors the source wallet's on-chain balance. When the wallet is funded enough to cover
desired.from.amount, Tesser automatically republishesstep.signature_requestedwith a refreshedsignature_requested_at. Repeat the signing flow with the refreshed step. - Expiration. The retry loop continues until the wallet is funded or the parent resource hits
expires_at. If the resource expires, the funds-movement is terminal — create a new resource to retry.
For per-resource example payloads of the balance_updated webhook and the surrounding scenario context, see each guide's Balance Check section:
See Also
For the scenario-specific context around wallet-step signing in each resource type:
- Send a Payout (from a wallet) — wallet-source payouts to a counterparty wallet (stablecoin) or fiat off-ramp.
- Rebalance Funds — Scenario 3 — on-chain transfer between two self-custodial wallets.
- Withdraw Funds via a Liquidity Provider — Scenario 3 — on-chain transfer from a self-custodial wallet, then off-ramp at OpenFX.
For the funds-movement lifecycle, status taxonomies, and overlay model that contextualize wallet-step signing within a resource's broader flow: