Skip to main content
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:
StatusDescription
PendingTransaction created, awaiting policy approval or signing
BroadcastedSigned and sent to the network mempool
ConfirmedSuccessfully included in a block
FailedBroadcast succeeded but execution failed on-chain
RejectedBlocked 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)
For production systems, use webhooks to receive status updates in real-time instead of polling.

Relevant events

EventWhen it fires
wallet.transfer.requestedTransfer created
wallet.transfer.broadcastedTransfer sent to mempool
wallet.transfer.confirmedTransfer confirmed on-chain
wallet.transfer.failedTransfer failed
wallet.transfer.rejectedTransfer rejected by policy
wallet.transaction.requestedBroadcast transaction created
wallet.transaction.broadcastedTransaction sent to mempool
wallet.transaction.confirmedTransaction confirmed on-chain
wallet.transaction.failedTransaction failed
wallet.transaction.rejectedTransaction rejected by policy
wallet.blockchainevent.detectedIncoming 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

  1. Create your webhook endpoint (see Local development for testing)
  2. Create a webhook via API or dashboard
  3. Subscribe to the events you need
  4. 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.

Matching deposits to your records

When processing incoming payments, you need to match blockchain transactions to records in your system (e.g., customer deposits, invoice payments). Since the sender initiates the transaction, you can’t include your own tracking IDs in the transfer. Here are two strategies to solve this.

Strategy 1: Unique wallet per deposit

Assign a unique wallet address to each pending deposit or customer. When you receive a wallet.blockchainevent.detected event, you know exactly which deposit it belongs to based on the receiving wallet. How it works:
  1. When a customer initiates a deposit, create or reserve a wallet for that specific transaction
  2. Give the customer this wallet’s address to send funds to
  3. When the webhook fires for that wallet, match it to your pending deposit record
  4. After confirmation (plus a safety buffer), release the wallet back to your pool
// Simplified wallet pool pattern
async function reserveDepositWallet(depositId: string): Promise<string> {
  // Get an available wallet from your pool, or create one
  const wallet = await getAvailableWallet() ?? await createNewWallet()

  // Mark it as reserved in your database
  await db.walletReservations.create({
    walletId: wallet.id,
    depositId: depositId,
    reservedAt: new Date()
  })

  return wallet.address
}

// In your webhook handler
case 'wallet.blockchainevent.detected':
  const { walletId, blockchainEvent } = event.data

  if (blockchainEvent.direction === 'In') {
    // Find the deposit associated with this wallet
    const reservation = await db.walletReservations.findByWalletId(walletId)
    if (reservation) {
      await creditDeposit(reservation.depositId, blockchainEvent.value)
    }
  }
  break
Considerations:
  • Works on all networks
  • Pre-create a pool of wallets to avoid creation latency during deposit flow
  • Include a cooldown period before reusing wallets (to handle delayed transactions)
  • Monitor pool size and create new wallets as needed

Strategy 2: Memo fields (select networks)

Some networks support a memo (or equivalent) field that travels with the transaction. You can ask customers to include a reference code when sending funds, then match on that memo in the webhook. Supported networks:
NetworkField nameNotes
StellarmemoCommonly used by exchanges for deposit identification
CosmosmemoStandard field for transaction notes
TONmemoSupported on native and Jetton transfers
AlgorandmemoTransaction notes field
HederamemoSupported on all transfer types
XRP LedgerdestinationTagNumeric tag (0-4294967295)
Memo-based matching requires customers to correctly include the reference. Always have a fallback process for deposits with missing or incorrect memos.
When to use each strategy:
StrategyBest for
Unique wallet per depositHigh reliability, any network, automated systems
Memo fieldNetworks that support it, when UX allows for customer input
Many platforms use both: unique wallets as the primary method, with memo as an optional optimization on supported networks.

Confirmation times

Different networks have different finality characteristics:
NetworkAverage confirmationNotes
Ethereum~12 secondsTrue finality ~15 min
Polygon~2 secondsCheckpoints to L1
Arbitrum~0.5 secondsDepends on L1 posting
Solana~0.4 secondsNear-instant
Bitcoin~10 minutes6 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