This guide explains how to monitor transaction status after submitting transfers or broadcasts through Dfns.
Transaction lifecycle
When you submit a transaction via the Transfer API or Broadcast API, it goes through these states:
| Status | Description |
|---|
Pending | Transaction created, awaiting policy approval or signing |
Broadcasted | Signed and sent to the network mempool |
Confirmed | Successfully included in a block |
Failed | Broadcast succeeded but execution failed on-chain |
Rejected | Blocked by policy or approval rejected |
Polling
Simple approach for occasional status checks. Query the transfer or transaction until it reaches a terminal state.
async function waitForConfirmation(
walletId: string,
transferId: string,
maxAttempts = 60,
intervalMs = 5000
): Promise<Transfer> {
for (let i = 0; i < maxAttempts; i++) {
const transfer = await dfnsClient.wallets.getTransfer({
walletId,
transferId,
})
if (transfer.status === 'Confirmed') {
return transfer
}
if (transfer.status === 'Failed' || transfer.status === 'Rejected') {
throw new Error(`Transfer ${transfer.status}: ${transfer.reason}`)
}
await new Promise((resolve) => setTimeout(resolve, intervalMs))
}
throw new Error('Timeout waiting for confirmation')
}
// Usage
const transfer = await dfnsClient.wallets.transferAsset({
walletId,
body: { kind: 'Native', to: recipient, amount: '1000000000000000000' },
})
const confirmed = await waitForConfirmation(walletId, transfer.id)
console.log('Transaction hash:', confirmed.txHash)
Webhooks (recommended)
For production systems, use webhooks to receive status updates in real-time instead of polling.
Relevant events
| Event | When it fires |
|---|
wallet.transfer.requested | Transfer created |
wallet.transfer.broadcasted | Transfer sent to mempool |
wallet.transfer.confirmed | Transfer confirmed on-chain |
wallet.transfer.failed | Transfer failed |
wallet.transfer.rejected | Transfer rejected by policy |
wallet.transaction.requested | Broadcast transaction created |
wallet.transaction.broadcasted | Transaction sent to mempool |
wallet.transaction.confirmed | Transaction confirmed on-chain |
wallet.transaction.failed | Transaction failed |
wallet.transaction.rejected | Transaction rejected by policy |
wallet.blockchainevent.detected | Incoming deposit or other on-chain event |
See Webhook Events for the complete list and event data schemas.
Basic handler
app.post('/webhooks/dfns', express.json(), (req, res) => {
// Always respond quickly with 200
res.status(200).send('OK')
const event = req.body
switch (event.kind) {
case 'wallet.transfer.confirmed':
const { transferRequest } = event.data
console.log(`Transfer ${transferRequest.id} confirmed`)
console.log(`Tx hash: ${transferRequest.txHash}`)
// Update your database, notify user, etc.
break
case 'wallet.transfer.failed':
const { transferRequest: failed } = event.data
console.error(`Transfer ${failed.id} failed`)
// Alert, retry logic, etc.
break
case 'wallet.blockchainevent.detected':
const { blockchainEvent } = event.data
if (blockchainEvent.direction === 'In') {
console.log(`Received ${blockchainEvent.value} deposit`)
}
break
}
})
Always respond with 200 quickly, even if processing takes time. Use a queue for async processing. See Webhooks best practices for details.
Setup
- Create your webhook endpoint (see Local development for testing)
- Create a webhook via API or dashboard
- Subscribe to the events you need
- Verify webhook signatures to ensure events are from Dfns
Detecting deposits
Use the wallet.blockchainevent.detected event to detect incoming transfers:
case 'wallet.blockchainevent.detected':
const { blockchainEvent } = event.data
if (blockchainEvent.direction === 'In') {
switch (blockchainEvent.kind) {
case 'NativeTransfer':
console.log(`Native deposit: ${blockchainEvent.value}`)
break
case 'Erc20Transfer':
console.log(`ERC-20 deposit: ${blockchainEvent.value}`)
console.log(`Token: ${blockchainEvent.contract}`)
break
// Handle other transfer types...
}
}
break
Deposit detection (wallet.blockchainevent.detected) is only available for Tier-1 networks.
Confirmation times
Different networks have different finality characteristics:
| Network | Average confirmation | Notes |
|---|
| Ethereum | ~12 seconds | True finality ~15 min |
| Polygon | ~2 seconds | Checkpoints to L1 |
| Arbitrum | ~0.5 seconds | Depends on L1 posting |
| Solana | ~0.4 seconds | Near-instant |
| Bitcoin | ~10 minutes | 6 blocks for high confidence |
Dfns marks transactions as Confirmed when included in a block. For high-value transactions on chains with probabilistic finality, you may want additional application-level confirmation tracking.
Handling failures
Rejected by policy
if (transfer.status === 'Rejected') {
console.log('Rejected:', transfer.reason)
// e.g., "Blocked by policy: TransactionAmountLimit exceeded"
}
Solutions:
- Check your policy configuration
- Request approval if policy requires it
- Adjust transaction parameters
Failed on-chain
if (transfer.status === 'Failed') {
console.log('Failed:', transfer.reason)
// e.g., "execution reverted: insufficient balance"
}
Common causes:
- Insufficient balance (including gas)
- Smart contract revert
- Nonce issues
- Gas limit too low