Skip to main content

Webhooks & Notifications

Real-time push notifications delivered to your endpoint over HTTP, organized into three streams. Subscribe per vault to receive events as they happen.

Webhook Streams

StreamScopeWhat It Covers
OutgoingPer vaultFull lifecycle of transactions created within the platform
BlockchainPer vaultEvery on-chain transaction affecting a vault address (outgoing and external), as observed on-chain
IncomingPer vaultDeposits to vault addresses — from mempool detection through confirmations and rollbacks

A single webhook URL can serve any subset of the three streams.


Delivery Model

Understanding how notifications are delivered is essential to building a correct consumer.

Envelope

Every delivered webhook is a JSON object with two envelope fields plus the event-specific body:

FieldDescription
idUnique notification ID. Stable across re-deliveries of the same notification — use it for idempotency.
timestampDelivery timestamp, Unix epoch milliseconds.
stream"outgoing", "blockchain", or "incoming".
typeEvent type within the stream (see each stream below).
Remaining fields depend on stream + type.

Guarantees

  • At-least-once. A notification may be delivered more than once (retries, redeliveries after platform recovery). Make your handler idempotent — deduplicate on id, or on the natural key (blockchainHash, type, confirmations).
  • Per-vault ordering. Notifications for one vault are delivered sequentially in the order they were produced; if a delivery fails, the platform retries it before sending any later one for the same vault, so you won't receive a later event while an earlier one is still undelivered. There is no ordering guarantee across different vaults. Two caveats still make strict ordering unsafe to rely on: at-least-once means a notification (even an earlier mined) can be redelivered after a later one, and a rollback (chain reorg) legitimately arrives after the confirmations for the same hash. Make handlers idempotent (dedupe on id, or on (blockchainHash, type, confirmations)) and tolerant of a later event superseding an earlier one.
  • Success = HTTP 2xx. Any 2xx response marks the notification delivered. Any non-2xx, or a transport error (timeout, DNS, refused), counts as a failure and is retried.
  • Timeout. Each delivery attempt has a 10-second timeout. Respond 2xx quickly and do heavy processing asynchronously.

Retries & backoff

Failed notifications stay queued and are retried automatically with linear backoff (the retry delay grows with the number of consecutive failures). A persistently unreachable endpoint accumulates a backlog that keeps being retried until your endpoint recovers.

After you fix a broken endpoint, the backlog is delivered to your current URL, but the accumulated backoff still gates how soon retries resume. To resume promptly, call the retry-schedule nudge. Updating the subscription URL alone does not reset the backoff.

Security of the destination

Outbound deliveries are plain HTTP POST. The platform does not sign payloads (no HMAC) and does not authenticate to your endpoint beyond POSTing to the exact URL you configured. Protect your webhook receiver yourself:

  • Use an unguessable URL (include a long random path/secret) or terminate mTLS / an auth proxy in front of it.
  • Do not trust a webhook as the sole source of truth for funds movement — reconcile against the transactions API and the blockchain-tx list before crediting a customer.

Managing Subscriptions

Authorization

ActionRequired permissions
POST /{vaultId}, PUT /{vaultId}/confirmations, DELETE /{vaultId}, PATCH /{vaultId}/notifications/retry-schedule, replaymanageNotifications on the vault and manageVaults on the organization
GET /{vaultId}, GET /{vaultId}/confirmations, GET /notifications, replay (read paths)Above or global Vault:read

Without the required permissions the call returns 403.

Set up subscriptions

POST /api/v1/subscriptions/{vaultId} — configure which streams a vault delivers, and to which URL:

curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://your-app.com/webhooks/carabaas",
"streams": ["outgoing", "incoming", "blockchain"]
}' \
https://api.carabaas.com/api/v1/subscriptions/{vaultId}

Returns 200 with the created subscription rows.

Full override, not merge

POST replaces the vault's entire subscription set with the streams in the body. The webhookUrl applies to every stream in the call.

  • To remove one stream, POST the full remaining list without it.
  • To change the URL, POST the same streams with the new URL — subsequent deliveries, including any queued backlog, go to the new URL.
  • To stop everything, use DELETE.

Get subscription info

curl -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/subscriptions/{vaultId}

Delete subscriptions

Removes all subscriptions for the vault:

curl -X DELETE -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/subscriptions/{vaultId}

Confirmation settings

Each network publishes a fixed set of confirmation depths at which it emits confirmation events (its confirmations array — for example [1, 6, 12]: a shallow "first block" depth, an intermediate depth, and a deep-confirmation depth). By default a vault receives events at all of them.

PUT /api/v1/subscriptions/{vaultId}/confirmations sets, per network, the maximum depth you want notifications for — you receive every published depth up to and including your value, and deeper ones are suppressed. For a network publishing [1, 6, 12]: setting 6 delivers the depth-1 and depth-6 events but drops the deep 12; setting 12 (already the maximum) changes nothing.

curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"confirmations": {
"ethereum-mainnet": 6,
"bitcoin-mainnet": 3
}
}' \
https://api.carabaas.com/api/v1/subscriptions/{vaultId}/confirmations
Only published depths are valid

The value must be one of the depths that network actually publishes — look them up in the confirmations array from GET /networks. Submitting any other number (e.g. 5 for a network publishing [1, 6, 12]) is rejected with 400:

Confirmation number for network ethereum-mainnet must be one of 1, 6, 12

An unknown network key also returns 400. The call replaces the whole map for the vault — it does not patch a single network.

Get confirmation settings

curl -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/subscriptions/{vaultId}/confirmations

Returns { "confirmations": { [network]: maxDepth, … } }. An empty object means no override — you receive every depth the network publishes.

Recover from backoff

PATCH /api/v1/subscriptions/{vaultId}/notifications/retry-schedule — after fixing a broken endpoint, pull the vault's next retry forward so the queued backlog is attempted promptly instead of waiting out the accumulated backoff:

curl -X PATCH -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/subscriptions/{vaultId}/notifications/retry-schedule

Returns { "nextTryAt": <timestamp | null> }. null means the vault is already eligible for retry. This is pull-forward only — it never delays an already-imminent retry, and it is a no-op if the vault has no backlog.


Outgoing Stream

Tracks the full lifecycle of transactions created within the platform. Each meaningful state transition emits a notification.

Transaction lifecycle

Progress events (happy path)

TypeExtra fieldsDescription
approval-pendingquorum, approvalsQuorum-gated transaction created; waiting for M-of-N approvals. Emitted at create time. Not sent for no-quorum orders.
approvedquorum, approvalsOne approval recorded (emitted once per approval step until quorum is met).
pendingquorum, approvalsNo-quorum order accepted for signing (status becomes requested). Not emitted for quorum-gated orders. May arrive near requested — both are informational.
requestedTransaction queued for signing / blockchain attempt created.
initedBlockchain transaction built.
to-signSigning payloads prepared; awaiting your signatures. External-signer vaults only.
signedSignature complete.
submittedblockchainHashBroadcast to the blockchain.
minedblockchainHash, balanceChanges, metadataConfirmed on-chain, with balance changes.

‡ External signerto-sign is emitted only for vaults configured with an external signer, where signing is not performed by the platform. It signals that the prepared signing payloads are ready for you to sign and submit back via the API; the transaction advances to signed once your signatures are accepted. Vaults signed by the platform never emit to-sign.

Vaults with master-approvals enabled use GET /api/v1/transactions/{txId}/master-approval-payload for the final signing bundle — there are no separate master-approval-* webhook types. Track them via approved, to-sign, and the standard progress events above.

Recommended consumer

Intermediate progress events are informational — not every transaction emits all of them, and delivery order is not guaranteed.

Drive your state machine from:

  • Terminal success: mined
  • Terminal failure: failed, cancelled, replaced, rolledback
  • External signer action: to-sign → sign via API
  • Transient pause: warning (status unchanged; only insufficient_balance and hosted_singner_unavailable are delivered as warnings)

Use submitted/mined as the primary on-chain progress pair. Poll GET /transactions/{id} or GET .../events when you need reasonDetails on insufficient_balance warnings.

Exception & failure events

TypeExtra fieldsDescription
warningreasonCodeProcessing paused for a transient reason; the transaction is still alive and retried automatically. Status is unchanged. Carries reasonCode only — no reasonDetails (fetch GET .../transactions/{id}/events for the breakdown on insufficient_balance).
cancelledreasonCodeTerminal. Cancelled — see reasonCode for the cause. Client declines end here with declined_by_client (there is no separate declined webhook).
failedblockchainHashTerminal. Failed on-chain (e.g. reverted) or during processing.
rolledbackblockchainHashTerminal. Rolled back after submission (chain reorg).
replacedblockchainHashTerminal. Superseded by a replacement transaction that mined (e.g. RBF).

Warning reasonCodes you'll receive:

reasonCodeMeaning
insufficient_balanceSource can't cover amount + fee at signing time. Top up the address — the platform picks it back up automatically.
hosted_singner_unavailableA hosted signer is temporarily unavailable (our side). Resolves automatically.

Other operational conditions (generic signer issues, network maintenance, and similar) do not surface as warning webhooks. If a paused condition never resolves within the platform's retry window, the transaction ends in cancelled with a matching reasonCode.

Cancellation reasonCodes: insufficient_balance, singner_unavailable, hosted_singner_unavailable, managed_singner_unavailable, shared_singner_unavailable, network_maintenance, not_enough_utxos, order_expired, invalid_order, transaction_expired, declined_by_client, rejected_by_approver, rejected_by_master_approver, replacement_status_invalid, hash_already_exists.

Common fields

Every outgoing notification carries the envelope (id, timestamp, stream: "outgoing", type) plus:

FieldDescription
vaultIdVault the transaction belongs to.
transactionIdPlatform transaction ID.
orderIdYour idempotency key for the order.
networkBlockchain network.
accountIdAccount within the vault (when the tx was scoped to an account/address).
addressIdSource address (when the tx was scoped to a specific address).
destinationAddressRecipient blockchain address.
memoDestination memo/tag, when set (Stellar, Ripple, Cosmos).
metadataNetwork-specific metadata (gas, gasPrice, fee, …).
blockchainHashOn-chain hash — present from submitted onward.
reasonCodePresent on warning, cancelled.
balanceChangesPresent on mined — see Understanding balanceChanges.
quorum, approvalsApproval state — present on pending / approval-pending / approved.
flowIdSet when the transaction is part of an approval flow.

Enriched context (in balanceChanges)

Each entry in a mined notification's balanceChanges carries the full account/address context, so you can attribute movement without extra API calls:

FieldDescription
account.nameAccount name (e.g. hot-wallets).
account.dataCustom metadata you set on the account.
address.nameAddress name (e.g. customer-12345).
address.networkAddressOn-chain address string.
address.hdpathHD derivation path.
address.dataCustom metadata you set on the address.
address.memoMemo/tag value (Stellar, Ripple, Cosmos).
Aggregation level changes the shape

On mined, entries are collapsed to the level the transaction was created at. Address-level entries carry both address and account; account-level entries carry only account; vault-level entries carry neither. See outgoing aggregation.

Example — mined

{
"id": "b0f5a1e2-8c3d-4e6f-9a1b-2c3d4e5f6a7b",
"timestamp": 1758633270000,
"stream": "outgoing",
"type": "mined",
"vaultId": "cai3GvhueQApTaCcUtjai9",
"network": "ethereum-sepolia",
"accountId": "eFjwUQXB8CMnrTHSgYzaL6",
"addressId": "gMP71sR5sNUnGdKFTsNzp6",
"destinationAddress": "0x3f06F88431d30067a7E87BB87957d008ADCc3bA7",
"orderId": "59850710-3998-4347-8c82-0d625bac9bbb",
"transactionId": "suoPF5JPqgo2NvMmMfR6mg",
"blockchainHash": "0x6a925762547de72487bb1cce0c95226e8e9cf24d...",
"metadata": {
"gas": "21000",
"gasUsed": "21000",
"gasPrice": "1200010",
"isContractCall": false
},
"balanceChanges": [
{
"asset": "c1",
"amount": "-14000000000000000",
"amountFormatted": {
"asset": { "name": "ETH Sepolia", "decimals": 18, "type": "native" },
"value": "-0.014"
},
"tags": [],
"address": {
"id": "gMP71sR5sNUnGdKFTsNzp6",
"networkAddress": "0xF3fC7B157615c5eD88f87d05f24C4C8E0c8D6A42",
"network": "@eth-like",
"hdpath": "m/44/60/0/0/1",
"name": "hot-wallet:eth-like",
"data": { "purpose": "operations" }
},
"account": { "id": "eFjwUQXB8CMnrTHSgYzaL6", "name": "hot-wallets", "data": {} }
},
{
"asset": "c1",
"amount": "-21000000000000",
"amountFormatted": {
"asset": { "name": "ETH Sepolia", "decimals": 18, "type": "native" },
"value": "-0.000021"
},
"tags": ["fee"],
"address": { "id": "gMP71sR5sNUnGdKFTsNzp6", "networkAddress": "0xF3fC7B157615c5eD88f87d05f24C4C8E0c8D6A42", "network": "@eth-like", "hdpath": "m/44/60/0/0/1", "name": "hot-wallet:eth-like", "data": {} },
"account": { "id": "eFjwUQXB8CMnrTHSgYzaL6", "name": "hot-wallets", "data": {} }
}
]
}

Example — warning

{
"id": "d7c1f2a3-4b5c-4d6e-8f90-1a2b3c4d5e6f",
"timestamp": 1758633267120,
"stream": "outgoing",
"type": "warning",
"reasonCode": "insufficient_balance",
"orderId": "77566f66-634e-4c0f-adce-2adc0bce7dd7",
"transactionId": "ooRPCgGMpeB1a13HG1Ajyj",
"network": "ethereum-sepolia",
"vaultId": "4inhhkumdUQAuvNnM77qQs",
"accountId": "43ta6eUh3qmWFZVmMCfNP1",
"addressId": "sd3hr3UQLFX5zUwPXqSk3o",
"destinationAddress": "0xe8d5a90ba593ad8fbc7d40de4dfc8b7f269f9cb2"
}

Blockchain Stream

Reports every on-chain transaction affecting a vault address — both your outgoing transactions and external third-party transfers — exactly as they appear on-chain. Scoped per vault. This is the detailed, per-entry view; balance changes are not aggregated here (see aggregation per stream).

Event types

TypeExtra fieldsDescription
transactionbalanceChanges, metadataA confirmed on-chain transaction touching a vault address.
mempoolbalanceChanges, metadataTransaction detected in the mempool (chains that expose one, e.g. Bitcoin).
confirmationconfirmationsConfirmation-count update for a tracked transaction (no balance changes).
rollbackA previously reported transaction was rolled back (chain reorg).

Common fields

Envelope (id, timestamp, stream: "blockchain", type) plus:

FieldDescription
vaultIdVault whose addresses are affected.
networkBlockchain network.
blockchainHashOn-chain transaction hash.
confirmationsCurrent confirmation count (on confirmation; may appear on others).
balanceChangesPer-address, un-aggregated changes — present on transaction / mempool only.
metadataNetwork-specific metadata — present on transaction / mempool.
transactionIdSet when the hash links to a platform transaction.

Incoming Stream

Tracks deposits to vault addresses, from mempool detection through confirmations and potential rollbacks.

Event types

TypeDescription
mempoolDeposit detected in the mempool before block inclusion.
confirmationDeposit confirmed, with the current confirmation count.
rollbackA previously seen deposit was rolled back after a reorg.

Common fields

Envelope (id, timestamp, stream: "incoming", type) plus:

FieldDescription
vaultIdVault receiving the deposit.
accountIdAccount within the vault.
addressIdReceiving address.
networkBlockchain network.
blockchainHashOn-chain transaction hash.
transactionIdPlatform transaction ID assigned to the deposit.
orderIdAuto-assigned order ID for the deposit.
confirmationsCurrent confirmation count.
balanceChangesAggregated balance changes for the receiving address — see below.
memoMemo/tag the deposit was routed with (Stellar, Ripple, Cosmos).
referenceDeposit reference, when present.
metadataNetwork-specific metadata (gas, contract call, …).

Example — deposit confirmation

{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"timestamp": 1758633000000,
"stream": "incoming",
"type": "confirmation",
"network": "ethereum-sepolia",
"blockchainHash": "0xc2cc27bdcee3e369a05fe740da898de8b8ace653...",
"vaultId": "cai3GvhueQApTaCcUtjai9",
"transactionId": "d6ZktNHf45cVdHTufyrwU2",
"orderId": "29270015-c058-40be-9303-003502234cf8",
"accountId": "eFjwUQXB8CMnrTHSgYzaL6",
"addressId": "gMP71sR5sNUnGdKFTsNzp6",
"confirmations": 1,
"metadata": { "gas": "21000", "gasUsed": "21000", "gasPrice": "1200015", "isContractCall": false },
"balanceChanges": [
{
"asset": "c1",
"amount": "49999974799685000",
"amountFormatted": {
"asset": { "name": "ETH Sepolia", "decimals": 18, "type": "native" },
"value": "0.049999974799685"
},
"tags": [],
"address": {
"id": "gMP71sR5sNUnGdKFTsNzp6",
"networkAddress": "0xF3fC7B157615c5eD88f87d05f24C4C8E0c8D6A42",
"network": "@eth-like",
"hdpath": "m/44/60/0/0/1",
"name": "customer-12345",
"data": { "mid": "cust-12345" }
},
"account": { "id": "eFjwUQXB8CMnrTHSgYzaL6", "name": "deposits", "data": {} }
}
]
}

Understanding balanceChanges

balanceChanges is an array describing how balances moved, per address + asset. It appears on mined (outgoing), on incoming (mempool/confirmation/rollback), and on blockchain transaction/mempool.

Entry shape

FieldDescription
assetUniversal Asset ID.
amountInteger string in the asset's smallest unit. Negative = debit, positive = credit. Never has a decimal point.
amountFormattedHuman-readable value for recognized assets: { asset: {...}, value } (decimal string, signed). Omitted for unrecognized assets.
tagsZero or more classification tags — see Tags.
addressEnriched address object (id, networkAddress, name, data, hdpath, memo). Absent on vault-level (merged) entries.
accountEnriched account object (id, name, data). Absent on vault-level (merged) entries.
memoInfoSub-account attribution for a registered-memo credit, when applicable.

Tags

Tags label what a change is. A single entry carries the tags that apply to it (usually one, sometimes none).

TagMeaning
feeNetwork fee, delivered as its own entry. On outgoing/mined you always receive a separate fee entry alongside the transfer, including on UTXO chains (see UTXO).
changeA UTXO change output returned to you. Where it surfaces depends on where it lands — see UTXO.
rebateA client-relevant credit (always positive). Kept as its own entry — never folded into the transfer amount.
mintA protocol-issued credit on the fee asset that the chain hands the sender as a side effect (e.g. NEO N3 auto-claims GAS, VeChain VTHO). Kept as its own entry; never nets against the fee or flips a change's direction.
erc20Marks an EVM token-contract (ERC-20) transfer (informational).
contractMarks a Tron token-contract (TRC-20) transfer (informational). Same concept as erc20, chain-specific name.
Forward-compatible

The tags above are the current set. Treat it as forward-compatible — ignore any tag you don't recognize rather than failing on it, in case new tags are introduced later.

Aggregation per stream

The same raw on-chain changes are presented at different granularity depending on the stream:

Stream / eventAggregationEntries per address+asset
blockchain transaction/mempoolNone — one entry per on-chain change, the detailed view. Multiple fee entries may appear separately.many
incoming mempool/confirmation/rollbackCombined per address+asset — the transfer as a single amount, with fee / rebate / mint each as their own entry.≤ 4
outgoing minedAggregated per address+asset (as above), then collapsed to the transaction's source level: address → per-address; account → merged per asset across the account's addresses; vault → merged per asset across the vault.≤ 4 per asset

Account-/vault-level collapsing matters mainly on UTXO networks, where one withdrawal can spend inputs from several addresses — without it you'd get one entry per input address instead of a single meaningful debit.

Conservation: don't double-count

A transaction's on-chain effects are split across its notifications so that each custody-side amount is counted exactly once:

  • The outgoing/mined notification carries the debits (what left your addresses) — the transfer and the fee.
  • Any credit that lands on a custody address — the destination of an internal transfer, or a UTXO change output sent to a different vault address — is excluded from outgoing/mined and delivered on its own incoming notification instead.

So the sum of a transaction's outgoing/mined entries and every related incoming entry equals the total custody-side movement — with nothing lost and nothing double-counted. Do not add a transaction's incoming amounts to its outgoing/mined amounts expecting them to overlap; they are complementary. Reconcile withdrawals from outgoing/mined, and deposits / internal-transfer destinations from incoming.

Memo-routed self-transfers

When a transaction sends from an address to itself with a memo (e.g. routing funds between memo sub-accounts), you receive two complementary notifications: the credit leg as an incoming, and the debit leg on outgoing/mined — so the movement is visible on both sides rather than cancelling out. If the memo is registered in your address book, memoInfo on the incoming credit attributes it to the sub-account.

UTXO networks: change outputs and fees

On UTXO chains (Bitcoin, etc.):

  • Fee — UTXO chains have no explicit fee output, but you still receive a clean [transfer, fee] pair on outgoing/mined (a separate fee-tagged entry). The total fee is also available in the transaction metadata.
  • Change output — a UTXO withdrawal returns the unspent remainder as a change output. How it appears depends on where it lands:
Change destinationHow it appears
Back to a source-side address (the default)Combined into that address's debit — you won't see a separate change entry.
To a different address in the same vaultIts own incoming credit, carrying the change tag so you can tell a self-change apart from a real external deposit.

Eventual consistency of the read API

The blockchain-tx list reflects the same balance changes as these notifications, but reports them per address. A just-mined transaction appears in the list right away, while its balanceChanges field may be briefly omitted until the change has been processed. Because the list is per-address (not collapsed to the transaction's source level), it reconciles with the outgoing/mined notification by sum rather than entry-for-entry.


Replay & List Notifications

Requires manageNotifications on the vault (or global Vault:read for read-only list) plus manageVaults on the organization — see Authorization.

Replay a notification

Re-deliver a previously delivered notification (debugging / recovery), optionally to a different URL:

curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://acme.com/webhook/vault-updates",
"deliveryId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}' \
https://api.carabaas.com/api/v1/subscriptions/notifications/{notificationId}/replay
  • url is required; deliveryId is optional (a fresh one is generated if omitted).
  • Only delivered notifications can be replayed — replaying one that is still pending returns 400.
  • A previously successful delivery is immutable — replaying a deliveryId whose recorded result was already 2xx returns 409.

List notifications for a transaction

GET /api/v1/subscriptions/notifications returns a transaction's notifications, merging pending and delivered entries (deduplicated, delivered wins), each with a status of pending or delivered. Notifications keyed on neither a transaction nor a blockchain hash (e.g. pure signing progress) are not returned here.

curl -H "Authorization: Bearer $TOKEN" \
"https://api.carabaas.com/api/v1/subscriptions/notifications?transactionId={transactionId}"

See Also