Changelog

Webhook payload fixes (2026-10-15)

Corrections to the October 2026 webhook payloads: one balance rule for every ledger and funds event, canonical network codes, real amounts, on-chain hashes only, the new user.funds.released event and the end of the legacy code body.

Release: webhook payload fixes · Date: 2026-10-15 · Environment: sandbox first, then production

This update corrects payloads of the October 2026 webhook release. Several fields carried values that the catalogue didn't promise: provider network names, zero amounts, internal transaction names and user totals instead of wallet balances. They now carry what Webhooks documents. It also adds user.funds.released and removes the legacy code notification body.

These changes take effect as soon as the update reaches your environment. The envelope doesn't change.

Before the update reaches your environment

Check these first. They break integrations that don't change:

  1. Read balances on ledger.wallet.* and user.funds.frozen as the balances of the wallet in the event, not the user's totals. held no longer includes deposits pending screening.
  2. Accept null in sweep_tx_hash, addresses[].network, new_addresses[].network and the amount of treasury holds and of treasury.contamination.remediated.
  3. Stop reading the withdrawal code from the legacy {"pin", …} body. Read it from user.withdrawal.verification_code_issued.

Removed: the legacy code notification

BeforeAfterWhat you must change
When a withdrawal code reached Sovera by email, your endpoint also received an unsigned body without the envelope: {"pin", "email", "atx_id", "amount", "currency", "destination_address"}.Not sent. user.withdrawal.verification_code_issued is the only delivery of the code.Read the code from the event. Your endpoint now receives only events and the test notification of Verify Webhook.

New event: user.funds.released

Sent when the review of a deposit frozen by user.funds.frozen approves it. The frozen amount becomes available in the same wallet, once, and ledger.wallet.credited with reason screening_cleared follows with the same balances. Resource: wallet, and resource_id is the wallet id.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "deposit_id": "96abb315-99d8-59e7-a333-1007dba37ad5",
  "amount": "150.00000000",
  "currency": "usdc",
  "reason_code": "screening_failed",
  "status": "ACTIVE",
  "hold_id": "hold_deposit_96abb315-99d8-59e7-a333-1007dba37ad5",
  "available": "470.00000000",
  "held": "0.00000000",
  "balance": "470.00000000"
}

deposit_id and hold_id are the same values as on user.funds.frozen. A rejected deposit stays frozen and sends no event. A hold announced before this update can't be released this way: approving that deposit moves no funds and sends no event. See user.funds.released.

Field changes by event

Resulting balances

EventBeforeAfterWhat you must update
ledger.wallet.added, ledger.wallet.credited (screening_cleared, transfer, withdrawal_reversal), ledger.wallet.debited (transfer), ledger.wallet.reversed, user.funds.frozenThe user's totals for the currency across all their wallets, with held = the amount pending screening and balance = available + that amountThe balances of the wallet named in the event: its available, its held (funds reserved for an operation that hasn't run yet) and balance = available + held. Exact decimal strings with at least 8 decimals.Apply the balances to the wallet in wallet_id, not to the user. Don't expect a pending deposit in held or balance.
ledger.wallet.debited (withdrawal)Already that wallet's balancesUnchangedNothing.

A deposit still pending screening, or frozen by user.funds.frozen, isn't part of available, held or balance. So ledger.wallet.added shows the wallet's balances without the new deposit, and user.funds.frozen no longer shows the frozen amount in held. On ledger.wallet.credited with reason withdrawal_reversal, the balances now have 8 decimals; amount keeps the share as recorded and can have fewer. See Resulting balances.

Network codes

EventBeforeAfterWhat you must update
user.wallet.created (addresses[].network)The custody provider's network name (networks/ethereum-testnet-sepolia), or "" when missingThe canonical network code (ETHEREUM, TRON, …), the same value List Deposit Addresses returns. null when the network is unknown or missing.Match networks by canonical code. Handle null.
treasury.wallet.created (addresses[].network), treasury.wallet.rotated (new_addresses[].network)The custody provider's network nameThe canonical network code, or null when unknownSame as above.

Treasury amounts

EventBeforeAfterWhat you must update
treasury.funds.frozen, treasury.funds.released (amount)Always 0.00000000The amount of the contaminated deposit the hold covers, the same amount treasury.deposit.received reported. null if the deposit carried no amount.Read the amount from the event.
treasury.contamination.remediated (amount)Always 0.00000000The balance of the contaminated currency left isolated in the old wallet, read when the clean funds are moved out. Else the contaminated deposit amount. null if neither is known.Read the amount from the event. Handle null.

Transaction hashes

EventBeforeAfterWhat you must update
user.deposit.swept_to_treasury (without batch_id)An internal custody transaction name, not a hashAlways null: when the sweep starts, its on-chain hash isn't known yetDon't expect a hash on this event.
user.deposit.swept_to_treasury (with batch_id)The hash, else the last part of an internal transaction nameThe hash, else nullHandle null.
treasury.contamination.remediated (sweep_tx_hash)The internal name of the first sweep, else ""The on-chain hash of the first sweep, else nullHandle null instead of "".

No event sends the hash again once the transaction confirms.

On this page