Webhooks

Overview

Webhooks provide real-time notifications for virtual account events. Instead of polling the API, configure a webhook endpoint to receive automatic updates when deposits arrive, payouts are processed, or account status changes.

Webhook Events

EventDescription
virtual_account.createdVirtual account has been created and is active
virtual_account.deposit_funds_receivedFunds have been received in the virtual account
virtual_account.deposit_payment_submittedCrypto payment submitted to blockchain (crypto mode)
virtual_account.deposit_payment_processedCrypto payment confirmed on blockchain (crypto mode)
payout.pendingPayout created and awaiting processing
payout.processingPayout is being routed to banking partner
payout.completedPayout successfully sent to recipient
payout.failedPayout failed
payout.returnedFunds returned after being sent
liquidation.deposit_receivedCrypto received at liquidation address
liquidation.payout_processingUSD payout being processed
liquidation.payout_completedUSD successfully sent to recipient
liquidation.payout_failedUSD payout failed

Virtual Account Events

virtual_account.created

Sent when a virtual account becomes active and ready to receive deposits.

{
  "event": "virtual_account.created",
  "data": {
    "event_id": "evt_550e8400-e29b-41d4-a716-446655440001",
    "virtual_account_id": "va_550e8400-e29b-41d4-a716-446655440002",
    "user_id": "550e8400-e29b-41d4-a716-446655440000",
    "type": "US_BANK",
    "bank": "portage",
    "status": "active",
    "source_deposit_instructions": {
      "currency": "usd",
      "bank_name": "Portage Bank",
      "bank_account_number": "1234567890",
      "bank_routing_number": "021000021",
      "bank_beneficiary_name": "John Doe"
    },
    "destination": null,
    "created_at": "2024-01-15T10:30:00Z"
  }
}

Note: For fiat mode accounts, destination is null. For crypto mode accounts, destination contains the wallet details.


Deposit Events

virtual_account.deposit_funds_received

Sent when USD/MXN funds arrive at the virtual account. This is the primary deposit event for both crypto mode and fiat mode accounts.

{
  "event": "virtual_account.deposit_funds_received",
  "data": {
    "event_id": "evt_550e8400-e29b-41d4-a716-446655440010",
    "virtual_account_id": "va_550e8400-e29b-41d4-a716-446655440002",
    "deposit_id": "dep_550e8400-e29b-41d4-a716-446655440011",
    "amount": "1000.00",
    "currency": "USD",
    "source": {
      "payment_rail": "ach_push",
      "sender_name": "Acme Corporation",
      "sender_bank_routing_number": "021000021",
      "trace_number": "123456789",
      "description": "Invoice payment #1234"
    },
    "created_at": "2024-01-15T14:30:00Z"
  }
}

Source Fields

FieldDescription
payment_railHow funds were sent: ach_push, wire, spei
sender_nameName of the sender (if available)
sender_bank_routing_numberSender's bank routing number (US)
trace_numberACH trace number for tracking
descriptionPayment description/memo

virtual_account.deposit_payment_submitted

Crypto mode only. Sent when the cryptocurrency transaction has been submitted to the blockchain.

{
  "event": "virtual_account.deposit_payment_submitted",
  "data": {
    "event_id": "evt_550e8400-e29b-41d4-a716-446655440012",
    "virtual_account_id": "va_550e8400-e29b-41d4-a716-446655440002",
    "deposit_id": "dep_550e8400-e29b-41d4-a716-446655440011",
    "amount": "980.00",
    "currency": "USDC",
    "destination": {
      "network": "solana",
      "address": "7tQJvFk8XZoaVRjLGcBdqN3hJq8VqBvGpPQy8R9xYwZ1"
    },
    "destination_tx_hash": "5KYmFMZ3qvX7h8sN...",
    "created_at": "2024-01-15T14:31:00Z"
  }
}

virtual_account.deposit_payment_processed

Crypto mode only. Sent when the cryptocurrency transaction is confirmed on the blockchain.

{
  "event": "virtual_account.deposit_payment_processed",
  "data": {
    "event_id": "evt_550e8400-e29b-41d4-a716-446655440013",
    "virtual_account_id": "va_550e8400-e29b-41d4-a716-446655440002",
    "deposit_id": "dep_550e8400-e29b-41d4-a716-446655440011",
    "amount": "980.00",
    "currency": "USDC",
    "destination": {
      "network": "solana",
      "address": "7tQJvFk8XZoaVRjLGcBdqN3hJq8VqBvGpPQy8R9xYwZ1"
    },
    "destination_tx_hash": "5KYmFMZ3qvX7h8sN...",
    "created_at": "2024-01-15T14:35:00Z"
  }
}

Payout Events

Note: Payout events only apply to fiat mode virtual accounts.

payout.pending

Sent when a payout is created and awaiting processing.

{
  "event": "payout.pending",
  "data": {
    "payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
    "virtual_account_id": "va_550e8400-e29b-41d4-a716-446655440003",
    "recipient_id": "550e8400-e29b-41d4-a716-446655440020",
    "amount": "1000.00",
    "currency": "USD",
    "fees": {
      "total_fees": 25
    },
    "recipient_amount": "975.00",
    "created_at": "2024-01-15T14:30:00Z"
  }
}

payout.processing

Sent when the payout is being routed to the banking partner.

{
  "event": "payout.processing",
  "data": {
    "payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
    "virtual_account_id": "va_550e8400-e29b-41d4-a716-446655440003",
    "status": "processing",
    "created_at": "2024-01-15T14:35:00Z"
  }
}

payout.completed

Sent when the payout has been successfully sent to the recipient's bank account.

{
  "event": "payout.completed",
  "data": {
    "payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
    "virtual_account_id": "va_550e8400-e29b-41d4-a716-446655440003",
    "recipient_id": "550e8400-e29b-41d4-a716-446655440020",
    "amount": "1000.00",
    "currency": "USD",
    "fees": {
      "total_fees": 25
    },
    "recipient_amount": "975.00",
    "recipient_currency": "USD",
    "completed_at": "2024-01-17T10:15:00Z"
  }
}

payout.failed

Sent when a payout fails.

{
  "event": "payout.failed",
  "data": {
    "payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
    "virtual_account_id": "va_550e8400-e29b-41d4-a716-446655440003",
    "amount": "1000.00",
    "currency": "USD",
    "reason": "compliance_rejected",
    "message": "Transaction did not pass compliance review",
    "created_at": "2024-01-15T14:35:00Z"
  }
}

Failure Reasons

ReasonDescription
compliance_rejectedTransaction did not pass compliance review
insufficient_balanceAccount balance became insufficient
invalid_recipientRecipient account details are invalid
recipient_bank_rejectedRecipient's bank rejected the transfer
compliance_holdTransaction held for manual compliance review

payout.returned

Sent when funds are returned after being sent (e.g., recipient bank rejected the transfer).

{
  "event": "payout.returned",
  "data": {
    "payout_id": "po_550e8400-e29b-41d4-a716-446655440010",
    "virtual_account_id": "va_550e8400-e29b-41d4-a716-446655440003",
    "amount": "1000.00",
    "currency": "USD",
    "reason": "recipient_account_closed",
    "message": "Recipient bank account has been closed",
    "returned_at": "2024-01-20T09:00:00Z"
  }
}

Return Reasons

ReasonDescription
recipient_account_closedRecipient's bank account is closed
recipient_bank_rejectedRecipient's bank rejected the transfer
incorrect_account_detailsAccount details don't match
insufficient_funds_at_recipientRecipient account cannot receive funds

Liquidation Events

Note: Liquidation events are for crypto-to-fiat off-ramping via liquidation addresses.

liquidation.deposit_received

Sent when crypto is received at a liquidation address.

{
  "event": "liquidation.deposit_received",
  "data": {
    "liquidation_id": "liq_550e8400-e29b-41d4-a716-446655440050",
    "virtual_account_id": "550e8400-e29b-41d4-a716-446655440001",
    "deposit_id": "ldep_550e8400-e29b-41d4-a716-446655440070",
    "network": "solana",
    "token": "USDC",
    "amount": "1000.00",
    "tx_hash": "5KYmFMZ3qvX7h8sN...",
    "sender_address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
    "usd_amount": "1000.00",
    "created_at": "2024-01-15T14:30:00Z"
  }
}

liquidation.payout_processing

Sent when the USD payout is being processed.

{
  "event": "liquidation.payout_processing",
  "data": {
    "liquidation_id": "liq_550e8400-e29b-41d4-a716-446655440050",
    "virtual_account_id": "550e8400-e29b-41d4-a716-446655440001",
    "deposit_id": "ldep_550e8400-e29b-41d4-a716-446655440070",
    "payout_id": "lpo_550e8400-e29b-41d4-a716-446655440080",
    "recipient_id": "rec_550e8400-e29b-41d4-a716-446655440060",
    "amount": "1000.00",
    "fees": {
      "total_fees": 15
    },
    "recipient_amount": "985.00",
    "status": "processing",
    "created_at": "2024-01-15T14:31:00Z"
  }
}

liquidation.payout_completed

Sent when USD has been successfully sent to the recipient.

{
  "event": "liquidation.payout_completed",
  "data": {
    "liquidation_id": "liq_550e8400-e29b-41d4-a716-446655440050",
    "virtual_account_id": "550e8400-e29b-41d4-a716-446655440001",
    "deposit_id": "ldep_550e8400-e29b-41d4-a716-446655440070",
    "payout_id": "lpo_550e8400-e29b-41d4-a716-446655440080",
    "recipient_id": "rec_550e8400-e29b-41d4-a716-446655440060",
    "amount": "1000.00",
    "fees": {
      "total_fees": 15
    },
    "recipient_amount": "985.00",
    "status": "completed",
    "completed_at": "2024-01-17T10:15:00Z"
  }
}

liquidation.payout_failed

Sent when a USD payout fails.

{
  "event": "liquidation.payout_failed",
  "data": {
    "liquidation_id": "liq_550e8400-e29b-41d4-a716-446655440050",
    "virtual_account_id": "550e8400-e29b-41d4-a716-446655440001",
    "deposit_id": "ldep_550e8400-e29b-41d4-a716-446655440070",
    "payout_id": "lpo_550e8400-e29b-41d4-a716-446655440080",
    "amount": "1000.00",
    "reason": "recipient_bank_rejected",
    "message": "Recipient bank rejected the transfer",
    "created_at": "2024-01-15T14:35:00Z"
  }
}

Handling Webhooks

Example: Express.js Handler

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/kira', (req, res) => {
  const event = req.body;

  console.log(`Received event: ${event.event}`);

  switch (event.event) {
    // Virtual Account Events
    case 'virtual_account.created':
      handleVirtualAccountCreated(event.data);
      break;

    // Deposit Events
    case 'virtual_account.deposit_funds_received':
      handleDepositReceived(event.data);
      break;
    case 'virtual_account.deposit_payment_submitted':
      handleCryptoSubmitted(event.data);
      break;
    case 'virtual_account.deposit_payment_processed':
      handleCryptoProcessed(event.data);
      break;

    // Payout Events
    case 'payout.pending':
      handlePayoutPending(event.data);
      break;
    case 'payout.processing':
      handlePayoutProcessing(event.data);
      break;
    case 'payout.completed':
      handlePayoutCompleted(event.data);
      break;
    case 'payout.failed':
      handlePayoutFailed(event.data);
      break;
    case 'payout.returned':
      handlePayoutReturned(event.data);
      break;

    // Liquidation Events
    case 'liquidation.deposit_received':
      handleLiquidationDeposit(event.data);
      break;
    case 'liquidation.payout_processing':
      handleLiquidationProcessing(event.data);
      break;
    case 'liquidation.payout_completed':
      handleLiquidationCompleted(event.data);
      break;
    case 'liquidation.payout_failed':
      handleLiquidationFailed(event.data);
      break;

    default:
      console.log(`Unhandled event type: ${event.event}`);
  }

  // Always respond 200 to acknowledge receipt
  res.status(200).send('OK');
});

function handleDepositReceived(data) {
  console.log(`Deposit received: ${data.amount} ${data.currency}`);
  console.log(`From: ${data.source.sender_name}`);
  // Update your database, notify user, etc.
}

function handlePayoutCompleted(data) {
  console.log(`Payout completed: ${data.payout_id}`);
  console.log(`Recipient received: ${data.recipient_amount} ${data.recipient_currency}`);
  // Update your database, notify user, etc.
}

function handlePayoutFailed(data) {
  console.log(`Payout failed: ${data.payout_id}`);
  console.log(`Reason: ${data.reason} - ${data.message}`);
  // Credit balance back, notify user, etc.
}

function handlePayoutReturned(data) {
  console.log(`Payout returned: ${data.payout_id}`);
  console.log(`Reason: ${data.reason}`);
  // Funds are automatically credited back to balance
  // Notify user, update records, etc.
}

function handleLiquidationDeposit(data) {
  console.log(`Crypto received: ${data.amount} ${data.token}`);
  console.log(`TX: ${data.tx_hash}`);
  // Notify user that crypto was received
}

function handleLiquidationCompleted(data) {
  console.log(`Liquidation payout completed: ${data.payout_id}`);
  console.log(`Recipient received: ${data.recipient_amount} USD`);
  // Notify user that USD was sent
}

function handleLiquidationFailed(data) {
  console.log(`Liquidation payout failed: ${data.payout_id}`);
  console.log(`Reason: ${data.reason}`);
  // Handle failure, contact support
}

Event Flow by Account Mode

Crypto Mode Flow

sequenceDiagram
    participant Sender
    participant Kira
    participant Your Server
    participant Blockchain

    Sender->>Kira: Send USD/MXN
    Kira->>Your Server: virtual_account.deposit_funds_received
    Note over Kira: Convert to crypto
    Kira->>Blockchain: Submit transaction
    Kira->>Your Server: virtual_account.deposit_payment_submitted
    Blockchain-->>Kira: Confirmed
    Kira->>Your Server: virtual_account.deposit_payment_processed

Fiat Mode Flow

sequenceDiagram
    participant Sender
    participant Kira
    participant Your Server
    participant Banking Partner

    Note over Sender,Your Server: Deposit Flow
    Sender->>Kira: Send USD
    Kira->>Your Server: virtual_account.deposit_funds_received

    Note over Your Server,Banking Partner: Payout Flow
    Your Server->>Kira: POST /payout
    Kira->>Your Server: payout.pending
    Kira->>Your Server: payout.processing
    Kira->>Banking Partner: WIRE/SWIFT
    Banking Partner-->>Kira: Completed
    Kira->>Your Server: payout.completed

Liquidation Flow (Crypto to USD)

sequenceDiagram
    participant Sender
    participant Liquidation Address
    participant Kira
    participant Your Server
    participant Recipient Bank

    Sender->>Liquidation Address: Send USDC/USDT
    Liquidation Address->>Kira: Crypto received
    Kira->>Your Server: liquidation.deposit_received

    Note over Kira: Convert crypto to USD

    Kira->>Your Server: liquidation.payout_processing
    Kira->>Recipient Bank: ACH/WIRE/SWIFT
    Recipient Bank-->>Kira: Completed
    Kira->>Your Server: liquidation.payout_completed

Best Practices

  1. Always return 200 - Acknowledge receipt immediately, process asynchronously
  2. Handle duplicates - Use event_id or payout_id to detect duplicate deliveries
  3. Process idempotently - Ensure handling the same event twice doesn't cause issues
  4. Log all events - Keep records for debugging and reconciliation
  5. Set up monitoring - Alert on failed webhook deliveries or processing errors
  6. Handle all event types - Even if you only care about some, log unknown types
  7. Verify webhook signatures - See Webhooks Guide for signature verification

Retry Policy

If your endpoint doesn't respond with a 2xx status code, webhooks are retried:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
612 hours

After 6 failed attempts, the webhook is marked as failed and no further retries are made.


Related Documentation