Webhooks

What webhooks are for, how Sovera delivers them, how to verify and process them safely, and every event your endpoint can receive.

Webhooks are how the platform tells your backend that something happened, without you asking. A deposit lands, a withdrawal is paid out, a transfer fails, a balance changes: Sovera sends an event to the HTTPS endpoint you registered, right when it happens.

Use them to:

  • React in real time instead of polling. Show a pending deposit on user.deposit.received, and make it spendable on ledger.wallet.credited. Mark a withdrawal as sent on treasury.withdrawal.executed or custody.withdrawal.executed.
  • Receive verification codes. When a user starts a withdrawal or a transfer, the code that confirms it is delivered only by webhook (user.withdrawal.verification_code_issued, user.transfer.verification_code_issued). The API never returns it.
  • Keep your ledger in step with Sovera's, using the ledger.* events and the resulting balances they carry.

Register and test your endpoint in Customer Webhooks. Every customer has one endpoint, and it receives every event.

How events are delivered

Every event arrives as an HTTPS POST with Content-Type: application/json, the headers you configured on your endpoint, and this body:

{
  "event_id": "0f7d2c4e-8a1b-4c3d-9e5f-6a7b8c9d0e1f",
  "event_type": "user.withdrawal.verification_code_issued",
  "occurred_at": "2026-10-14T14:03:12.081Z",
  "resource_type": "withdrawal",
  "resource_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
  "sequence_number": 1,
  "data": { }
}
FieldMeaning
event_idUnique per event. Use it to drop duplicates: the same event can arrive more than once.
event_typeWhat happened, in dotted lowercase (user.deposit.received). See Event catalogue.
occurred_atWhen it happened, UTC, with milliseconds.
resource_type, resource_idThe resource the event is about (deposit, withdrawal, transfer, wallet, batch, gas) and its id. Each event in the catalogue says which one it uses.
sequence_numberStarts at 1 and grows by one for each event about the same resource (same resource_type and resource_id). Use it to apply the events of one resource in order.
dataThe event payload. Its fields depend on event_type.

The body has only these seven keys. The name of the event is in event_type; the body has no top-level event key.

Retries and timeouts

  • Answer with any 2xx status as soon as you've stored the event. Do the real work afterwards.
  • A network error, a timeout or a 5xx answer is retried. Sovera makes up to 3 attempts in total, waiting 1 second and then 2 seconds between them.
  • A 4xx answer isn't retried. Sovera takes it as a permanent refusal.
  • Each attempt times out after 10 seconds.
  • Redirects aren't followed. A 3xx answer counts as a failed attempt.
  • After the last failed attempt the event isn't sent again. It stays recorded with delivery_status: SUSPENDED. List it with GET /v1/events.
  • An event produced while no endpoint is configured is recorded too, and never sent.

Security

  • Your endpoint must be HTTPS on a public host. Sovera checks the address it resolves to before every attempt. It never delivers to private, loopback or internal addresses, and it doesn't retry an endpoint that fails this check.
  • Add the auth headers your backend expects (an API key, a bearer token) when you register the endpoint. They're sent with every event and never shown back in full.
  • When Sovera has set a signing secret for your endpoint, every delivery carries X-Signature-256: sha256=<hex>. It's the HMAC-SHA256 of the raw request body with that secret. Compute it over the exact bytes you received, before parsing the JSON. Compare it in constant time, and reject the event if it doesn't match.

Other bodies your endpoint can receive

One body reaches the same URL without the envelope above: the test notification. Verify Webhook sends a body with "test": true and no event_type. Ignore it.

Every other body is an event in the envelope above. That includes withdrawal codes that reach Sovera by email: they arrive only as user.withdrawal.verification_code_issued.

Processing events safely

  1. Verify, store, answer. Check the signature (or your auth header), store the raw event keyed by event_id, and answer 2xx. Process it from your own queue.
  2. Be idempotent. If an event_id is already stored, answer 2xx and do nothing else.
  3. Order per resource. Events about different resources can arrive in any order. For one resource, apply them in sequence_number order, and skip an event whose number is lower than the last one you applied.
  4. Tolerate the unknown. New event types, new fields and new enum values can be added at any time. Ignore an event_type you don't handle, ignore fields you don't recognise, and treat an unknown reason_code as a generic failure.
  5. Reconcile. If your endpoint was down, list what you missed with GET /v1/events (filter by created_at, type, resource_type, resource_id or delivery_status). Then fetch each resource's current state with its GET endpoint.
  6. Handle verification codes with care. Show a code only to the user it belongs to, never log it, and discard it once it's used or expired (expires_at). Sovera never stores the code and never returns it from GET /v1/events.

Reading data

  • Amounts and balances are decimal strings, such as "25.00000000". Don't parse them as floats, and don't assume a fixed number of decimals.
  • Currency codes in data are lowercase asset codes (btc, eth, usdc). The API answers with uppercase codes, so compare them case-insensitively.
  • Timestamps are ISO-8601 in UTC.
  • An optional field that doesn't apply is left out. It isn't sent as null. The exception is a field the catalogue describes as nullable, such as sweep_tx_hash, addresses[].network or the amount of a treasury hold: it's always present, and null means the value isn't known.
  • resource_type is lowercase in the webhook body. GET /v1/events returns the same value in UPPER_SNAKE (DEPOSIT).

Event catalogue

These are all the events Sovera sends today.

GroupEvents
Verification codesuser.withdrawal.verification_code_issued, user.transfer.verification_code_issued
Depositsuser.deposit.received, user.deposit.screening_passed, user.deposit.screening_failed, user.deposit.swept_to_treasury, user.deposit.returned, user.deposit.failed, user.deposit.suspended_address_received, custody.deposit.confirmed
Funds holdsuser.funds.frozen, user.funds.released, treasury.funds.frozen, treasury.funds.released
Ledgerledger.wallet.added, ledger.wallet.credited, ledger.wallet.debited, ledger.wallet.reversed, ledger.transfer.failed
Withdrawalstreasury.withdrawal.requested, treasury.withdrawal.approved, treasury.withdrawal.executed, treasury.withdrawal.failed, custody.withdrawal.requested, custody.withdrawal.executed
Treasurytreasury.batch.executed, treasury.batch.failed, treasury.deposit.received, treasury.deposit.screened, treasury.wallet.flagged, treasury.wallet.created, treasury.contamination.remediated, treasury.wallet.rotated
Walletsuser.wallet.created
Gasgas.primary.threshold_breached, gas.backup.activated, gas.backup.threshold_breached

The examples below show only data. The envelope around it is always the one in How events are delivered.

Verification codes

These two events are the only channel that carries a verification code.

user.withdrawal.verification_code_issued

Sent when a user's crypto withdrawal needs a code: on initiate, and on resend. Resource: withdrawal, and resource_id is the withdrawal id.

A withdrawal follows one of two routes, and the event differs slightly:

Treasury pays directlyTreasury replenished first
How the code reaches SoveraSovera issues it.By email, read by your email poller.
Code lifetime5 minutes.15 minutes from when the email arrives.
triggerissued on initiate, resent on resend.Always issued, also after a resend.
attempts_remainingSent (3 with every new code).Not sent. Custody counts the attempts.
networkSent.Can be absent.
{
  "code": "482913",
  "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
  "withdrawal_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
  "expires_at": "2026-10-14T14:08:12.081Z",
  "trigger": "issued",
  "attempts_remaining": 3,
  "amount": "25.00000000",
  "currency": "usdc",
  "network": "ETHEREUM",
  "destination_address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72"
}
{
  "code": "731406",
  "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
  "withdrawal_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
  "expires_at": "2026-10-14T14:19:40.512Z",
  "trigger": "issued",
  "amount": "25.00000000",
  "currency": "usdc",
  "network": "ETHEREUM",
  "destination_address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72"
}
FieldMeaning
codeThe 6-digit code. Pass it to the confirm endpoint.
expires_atWhen the code stops working.
triggerissued or resent.
attempts_remainingConfirmation attempts left with this code.
networkCanonical network code of the withdrawal.
destination_addressWhere the funds go. Show it next to the code, so the user knows what they approve.

user.transfer.verification_code_issued

Sent when a wallet-to-wallet transfer is created, and when you resend its code. Resource: transfer, and resource_id is the transfer id. The code lasts 5 minutes and allows 3 attempts.

{
  "user_id": "5299e81c-3a8c-5b64-a112-b93bf43de74b",
  "transfer_id": "c2a4e6f8-1b3d-4f5a-9c7e-0d2f4a6b8c1e",
  "code": "907215",
  "expires_at": "2026-10-14T14:45:02.517Z",
  "trigger": "issued",
  "attempts_remaining": 3
}

trigger is issued on create and resent on resend. attempts_remaining is 3 with every new code.

Deposits

A deposit to a user's address moves through these events:

  1. user.deposit.received and ledger.wallet.added: the deposit is seen and stays pending until screening ends.
  2. user.deposit.screening_passed and ledger.wallet.credited: the funds become available. Or user.deposit.screening_failed: see that event for what follows.
  3. user.deposit.swept_to_treasury and custody.deposit.confirmed: the funds move on to the treasury and to custody.

All user.deposit.* events use resource deposit, and resource_id is the deposit id.

user.deposit.received

Sent when a deposit to one of a user's addresses is first seen on chain, before screening. The funds stay pending, and can't be spent, until screening ends.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "deposit_id": "96abb315-99d8-59e7-a333-1007dba37ad5",
  "amount": "150.00000000",
  "currency": "usdc",
  "source_address": "0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE",
  "tx_hash": "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060"
}

source_address can be absent when the deposit has several senders. tx_hash can be absent while the transaction isn't published yet.

user.deposit.screening_passed

Sent when every sender of the deposit screened as low risk. ledger.wallet.credited follows, with reason screening_cleared.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "deposit_id": "96abb315-99d8-59e7-a333-1007dba37ad5"
}

user.deposit.screening_failed

Sent when screening rejects the deposit. The deposit address is deactivated.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "deposit_id": "96abb315-99d8-59e7-a333-1007dba37ad5",
  "reason_code": "high_risk"
}
reason_codeMeaningWhat follows
elevated_riskA sender has medium risk.Sovera returns the deposit to its sender: ledger.wallet.reversed, then user.deposit.returned.
high_riskA sender has high risk.Same as elevated_risk.
severe_riskA sender has severe risk.Same as elevated_risk.
blacklistedThe sender or the destination address is blocked.The funds stay frozen for review: user.funds.frozen.

A deposit that can't be screened, or whose screening service stays unavailable, sends no event. It stays pending for manual review.

user.deposit.swept_to_treasury

Sent when a cleared deposit starts moving from the user's address to the treasury. It's sent again, with batch_id, when the batch that contains the deposit is forwarded from the treasury.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "deposit_id": "96abb315-99d8-59e7-a333-1007dba37ad5",
  "sweep_tx_hash": null,
  "destination": "treasury"
}
{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "deposit_id": "96abb315-99d8-59e7-a333-1007dba37ad5",
  "sweep_tx_hash": "0x9a1f3c7e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f2a4c6e8b0d1f3a",
  "destination": "treasury",
  "batch_id": "7e2d9c41-5b8a-4f3e-a1d6-2c9b8e7f6a50"
}
FieldMeaning
sweep_tx_hashThe on-chain hash of the transaction, or null when it isn't known. Always null on the first event, because the sweep has only started. On the event with batch_id, the hash of the transaction that forwarded the batch, or null if it isn't known yet. No event sends the hash again once the transaction confirms.
destinationAlways treasury.
batch_idOnly on the second event. Matches treasury.batch.*.

user.deposit.returned

Sent when the automatic return of a rejected deposit settles on chain.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "deposit_id": "96abb315-99d8-59e7-a333-1007dba37ad5",
  "destination_address": "0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE",
  "return_tx_hash": "0x1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d2f4a6c8e0b1d3f"
}

destination_address is the address that sent the deposit.

user.deposit.failed

Sent when a signing policy refused the transaction that moves the deposit on. The deposit stays where it is, and Sovera doesn't retry it automatically.

{
  "deposit_id": "96abb315-99d8-59e7-a333-1007dba37ad5",
  "reason_code": "policy_rejected"
}

reason_code is always policy_rejected today. The payload also defines an optional wallet_id. It's left out when the deposit can't be matched to a wallet, which is the case for every event sent today.

user.deposit.suspended_address_received

Sent when funds arrive at a deposit address that was deactivated. The deposit isn't processed.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "deposit_id": "5e7a9c1b-3d5f-4a7c-9e1b-3d5f7a9c1b3d",
  "amount": "40.00000000",
  "currency": "usdc",
  "source_address": "0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE",
  "destination_address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
  "tx_hash": "0x2e4a6c8e0b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d2f4a",
  "reason_code": "address_suspended"
}

destination_address is the deactivated address. reason_code is always address_suspended.

custody.deposit.confirmed

Sent when the transaction that forwards a batch to custody settles on chain. One event goes out per deposit in the batch. Resource: deposit, and resource_id is the deposit id.

{
  "reference_id": "7e2d9c41-5b8a-4f3e-a1d6-2c9b8e7f6a50",
  "amount": "150.00000000",
  "currency": "usdc"
}

reference_id is the batch id. amount is the deposit's amount.

Funds holds

user.funds.frozen

Sent when a deposit is blocked (user.deposit.screening_failed with blacklisted). Its funds stay frozen for review. 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": "FROZEN",
  "hold_id": "hold_deposit_96abb315-99d8-59e7-a333-1007dba37ad5",
  "available": "320.00000000",
  "held": "0.00000000",
  "balance": "320.00000000"
}
FieldMeaning
amountThe frozen amount.
reason_codeWhy the funds are frozen. Always screening_failed today.
statusAlways FROZEN on this event.
hold_idIdentifies this hold.
available, held, balanceThe balances of this wallet after the change. The frozen amount isn't part of them. See Resulting balances.

Sovera then reviews the deposit. If the review approves it, user.funds.released follows. If it rejects it, the funds stay frozen and no event is sent.

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. 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"
}
FieldMeaning
deposit_id, hold_idThe same values as on user.funds.frozen. Match the two events by hold_id.
amountThe released amount.
reason_codeWhy the funds were frozen: screening_failed.
statusAlways ACTIVE on this event: the funds can be spent.
available, held, balanceThe balances of this wallet after the release, now including the released amount. See Resulting balances.

A hold announced before the 2026-10-15 update can't be released this way: approving that deposit moves no funds and sends no event.

treasury.funds.frozen and treasury.funds.released

Sent during a treasury wallet rotation (see Treasury). treasury.funds.frozen is sent when the rotation starts, and treasury.funds.released when it ends. Both use the same hold_id. Resource: wallet, and resource_id is the contaminated treasury wallet.

{
  "amount": "500.00000000",
  "currency": "usdc",
  "reason_code": "contamination",
  "scope": "treasury",
  "hold_id": "hold_5a7c9e1b-3d2f-4a6b-8c0d-1e2f3a4b5c6d"
}
FieldMeaning
amountThe amount of the contaminated deposit the hold covers: the same amount treasury.deposit.received reported. null if that deposit carried no amount.
currencyThe asset of the deposit that triggered the rotation.
reason_codeAlways contamination today.
scopeAlways treasury.
hold_idThe same value on the frozen and the released event.

Ledger

Ledger events report balance movements in your users' wallets. All ledger.wallet.* events use resource wallet, and resource_id is the wallet id.

Resulting balances

Every ledger.wallet.* and user.funds.* event carries the balances that result from the change. They follow one rule: they're the balances, in the event's currency, of the wallet named in the event (wallet_id, also the resource_id). They're never the user's totals across wallets.

FieldMeaning
availableThat wallet's funds that can be spent.
heldThat wallet's funds reserved for an operation that hasn't run yet, such as a withdrawal that hasn't been debited.
balanceavailable plus held.

A deposit still pending screening isn't part of any of them, and neither is a deposit frozen by user.funds.frozen. Its amount reaches available and balance when ledger.wallet.credited (or user.funds.released) makes it spendable. The balances are exact decimal strings with at least 8 decimals.

ledger.wallet.added

Sent with user.deposit.received. The deposit enters the wallet as pending. Pending funds aren't part of the balances, so the balances don't include the new deposit yet.

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

ledger.wallet.credited

Sent when funds become available in a wallet.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "amount": "150.00000000",
  "currency": "usdc",
  "reason": "screening_cleared",
  "available": "470.00000000",
  "held": "0.00000000",
  "balance": "470.00000000"
}
reasonWhenExtra field
screening_clearedA deposit passed screening, or a frozen deposit was approved (right after user.funds.released). Its pending funds become available.—
transferThe receiving wallet of a transfer.transfer_id
withdrawal_reversalA withdrawal failed after its funds were debited. The funds come back, one event per wallet that paid.withdrawal_id

On withdrawal_reversal, amount is that wallet's share as it was recorded, and it can have fewer than 8 decimals. The balances always have at least 8.

ledger.wallet.debited

Sent when funds leave a wallet.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "amount": "25.00000000",
  "currency": "usdc",
  "reason": "withdrawal",
  "available": "445.00000000",
  "held": "0.00000000",
  "balance": "445.00000000"
}
reasonWhenExtra field
withdrawalA confirmed withdrawal spent its reserved funds. A withdrawal can be paid from several wallets: one event goes out per wallet, with that wallet's amount.—
transferThe sending wallet of a transfer.transfer_id

The withdrawal debit doesn't carry the withdrawal id. Match it through the withdrawal's debited_wallets.

ledger.wallet.reversed

Sent when the automatic return of a rejected deposit starts. The deposit's pending funds leave the wallet. They were never part of the balances, so the balances don't drop.

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

ledger.transfer.failed

Sent when a transfer can't go through. Resource: transfer, and resource_id is the transfer id.

{
  "transfer_id": "c2a4e6f8-1b3d-4f5a-9c7e-0d2f4a6b8c1e",
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "amount": "10.00000000",
  "currency": "usdc",
  "reason_code": "invalid_otp"
}

wallet_id is the sending wallet.

reason_codeMeaning
invalid_otpThe third wrong code blocked the transfer.
account_frozenThe account is frozen. The transfer stays PENDING.
insufficient_available_balanceThe available balance dropped before confirm. The transfer is FAILED.
unknownThe ledger couldn't move the funds. The transfer is FAILED.

Withdrawals

All withdrawal events use resource withdrawal, and resource_id is the withdrawal id. The events depend on the route:

  • Treasury pays directly: treasury.withdrawal.requested, then treasury.withdrawal.approved when the code is accepted, ledger.wallet.debited, and treasury.withdrawal.executed.
  • Treasury replenished first: custody.withdrawal.requested, then ledger.wallet.debited and custody.withdrawal.executed when custody accepts the code.

A failure or a cancellation sends treasury.withdrawal.failed on either route. No event announces that a withdrawal settled on chain: read the withdrawal to follow it to COMPLETED. If it fails after the debit, ledger.wallet.credited with reason withdrawal_reversal gives the funds back.

treasury.withdrawal.requested

Sent when a withdrawal paid directly by the treasury is initiated.

{
  "withdrawal_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
  "amount": "25.00000000",
  "currency": "usdc",
  "address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72"
}

address is the destination address. A fiat withdrawal sends this event too, with an empty address.

treasury.withdrawal.approved

Sent when the code is accepted and the withdrawal moves to PROCESSING.

{
  "withdrawal_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d"
}

treasury.withdrawal.executed

Sent when the treasury has sent the payment.

{
  "withdrawal_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
  "amount": "25.00000000",
  "currency": "usdc",
  "address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
  "tx_hash": "0x7b9d2f4a6c8e0b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d"
}

tx_hash can be absent when it isn't known yet.

treasury.withdrawal.failed

Sent when a withdrawal fails or is cancelled.

{
  "withdrawal_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
  "reason_code": "cancelled"
}
reason_codeMeaning
cancelledYou cancelled the withdrawal.
insufficient_fundsThe funds didn't cover the payout.
insufficient_gasThe gas balance didn't cover the network fee.
invalid_addressThe destination address was refused.
invalid_otpThe code was refused at execution.
network_errorThe network call failed.
custody_unavailableCustody couldn't process it.
account_frozenThe account is frozen.
policy_rejectedA signing policy refused the transaction.
unknownAny other error.

custody.withdrawal.requested

Sent when a withdrawal that needs the treasury to be replenished first is initiated.

{
  "withdrawal_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
  "amount": "25.00000000",
  "currency": "usdc"
}

custody.withdrawal.executed

Sent when custody accepts the code of such a withdrawal and its funds are debited.

{
  "withdrawal_id": "3b1f0c52-7d4e-4a9b-8c21-5e6f7a8b9c0d",
  "amount": "25.00000000",
  "currency": "usdc",
  "tx_hash": "0x7b9d2f4a6c8e0b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d"
}

tx_hash can be absent when custody doesn't return it.

Treasury

These events describe your treasury: deposit batches, deposits from outside your users, and wallet rotation.

treasury.batch.executed

Sent when every transfer of a deposit batch was sent from the treasury to custody. Resource: batch, and resource_id is the batch id.

{
  "batch_id": "7e2d9c41-5b8a-4f3e-a1d6-2c9b8e7f6a50",
  "amount": "1250.00000000",
  "currency": "usdc",
  "transactions": [
    {
      "tx_hash": "0x4c6e8b0d1f3a5c7e9b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f2a4c6e",
      "amount": "1250.00000000",
      "currency": "usdc"
    }
  ]
}

amount is the batch total. transactions lists each on-chain transfer.

treasury.batch.failed

Sent when a batch couldn't be sent, in full or in part. Resource: batch.

{
  "batch_id": "7e2d9c41-5b8a-4f3e-a1d6-2c9b8e7f6a50",
  "reason_code": "partial_failure",
  "succeeded_count": 1,
  "failed_count": 1
}
reason_codeMeaning
partial_failureSome transfers were sent and some failed.
all_transfers_failedNo transfer was sent.
no_deposit_addressNo custody deposit address was available.
policy_rejectedA signing policy refused the batch transaction.

succeeded_count and failed_count count transfers. With no_deposit_address, failed_count is the number of deposits.

treasury.deposit.received

Sent when funds reach the treasury from an address that isn't one of your users' addresses. Resource: deposit, and resource_id is the deposit id.

{
  "deposit_id": "5a7c9e1b-3d2f-4a6b-8c0d-1e2f3a4b5c6d",
  "amount": "500.00000000",
  "currency": "usdc",
  "source_address": "0x281055Afc982d96fAB65b3a49cAc8b878184Cb16",
  "source": "external",
  "tx_hash": "0x8e0b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d2f4a6c8e0b"
}

source is always external.

treasury.deposit.screened

Sent when screening of that deposit returns a result. Resource: deposit.

{
  "deposit_id": "5a7c9e1b-3d2f-4a6b-8c0d-1e2f3a4b5c6d",
  "risk_result": "low"
}

risk_result is low, medium, high or severe. If screening returns no result, no event is sent. A medium, high or severe result starts a treasury wallet rotation.

Treasury wallet rotation

When an outside deposit to the treasury is risky, Sovera replaces the treasury wallet. The events arrive in this order:

  1. treasury.wallet.flagged: the old wallet is flagged as contaminated.
  2. treasury.wallet.created: the new wallet is ready.
  3. treasury.funds.frozen: see Funds holds.
  4. treasury.contamination.remediated: the clean funds were moved to the new wallet.
  5. treasury.funds.released.
  6. treasury.wallet.rotated: the new wallet replaces the old one for future sweeps.

All of them use resource wallet. resource_id is the old wallet, except on treasury.wallet.created, where it's the new one.

treasury.wallet.flagged

{
  "wallet_id": "4f8e2a1c9b7d",
  "reason_code": "contamination"
}

reason_code is always contamination today.

treasury.wallet.created

{
  "wallet_id": "9c3b7e1f2a6d",
  "addresses": [
    {
      "address_id": "e1a7c3b9d5f2",
      "currency": "eth",
      "network": "ETHEREUM",
      "address": "0x9d1F2a3B4c5D6e7F8a9B0c1D2e3F4a5B6c7D8e9F"
    }
  ]
}

addresses lists one entry per network of the new wallet. currency is the network's native asset. network is the canonical network code, or null if the network isn't one Sovera supports.

treasury.contamination.remediated

{
  "wallet_id": "4f8e2a1c9b7d",
  "amount": "500.00000000",
  "currency": "usdc",
  "sweep_tx_hash": "0x3b5d7f9a2c4e6b8d0f1a3c5e7b9d2f4a6c8e0b1d3f5a7c9e2b4d6f8a0c1e3b5d"
}
FieldMeaning
currencyThe asset that triggered the rotation. It stays in the old wallet.
amountThe balance of that asset left isolated in the old wallet, read when the clean funds are moved out. If it can't be read, the amount of the contaminated deposit. null if neither is known.
sweep_tx_hashThe on-chain hash of the first transaction that moved clean funds. null when nothing was moved or the hash isn't known yet.

treasury.wallet.rotated

{
  "wallet_id": "4f8e2a1c9b7d",
  "new_wallet_id": "9c3b7e1f2a6d",
  "new_addresses": [
    {
      "address_id": "e1a7c3b9d5f2",
      "currency": "eth",
      "network": "ETHEREUM",
      "address": "0x9d1F2a3B4c5D6e7F8a9B0c1D2e3F4a5B6c7D8e9F"
    }
  ]
}

new_addresses has the same entries as addresses on treasury.wallet.created.

Wallets

user.wallet.created

Sent when a wallet is created for one of your users. Resource: wallet, and resource_id is the wallet id.

{
  "wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
  "wallet_type": "virtual",
  "status": "ACTIVE",
  "addresses": [
    {
      "address_id": "2b7c1e9a-4d3f-4a8b-9c6e-5f1d2a3b4c5d",
      "currency": "usdc",
      "network": "ETHEREUM",
      "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"
    }
  ]
}
FieldMeaning
wallet_idThe wallet id the API returns.
wallet_typevirtual or real.
statusAlways ACTIVE on this event.
addressesThe wallet's deposit addresses, one per currency and network. It can be shorter than the wallet's networks if some addresses weren't ready yet.
addresses[].networkThe canonical network code, the same value List Deposit Addresses returns for that address. null if the network isn't one Sovera supports.

No separate event is sent for each address.

Gas

Gas events warn that the wallets paying network fees are running low. Sovera checks them before each transaction that needs gas, and sends an event every time a check finds a low balance. Resource: gas, and resource_id is primary or backup.

gas.primary.threshold_breached

Sent when the primary gas wallet is at or below its threshold.

{
  "source": "primary",
  "balance": "0.00800000",
  "threshold": "0.01000000",
  "currency": "eth",
  "remaining_transactions": 4
}
FieldMeaning
balanceThe wallet's balance in its native asset (currency).
thresholdThe configured low-balance threshold.
remaining_transactionsHow many more transactions the balance covers at the current fee estimate. Absent when there's no estimate.

gas.backup.activated

Sent when the primary gas wallet is empty, so the backup wallet has to cover the fees.

{
  "reason": "primary_depleted"
}

gas.backup.threshold_breached

Sent when the backup gas wallet is at or below its threshold. The fields are the same as on gas.primary.threshold_breached, with source: "backup".

{
  "source": "backup",
  "balance": "0.00200000",
  "threshold": "0.00500000",
  "currency": "eth",
  "remaining_transactions": 1
}

On this page