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
| Event | Description |
|---|---|
virtual_account.created | Virtual account has been created and is active |
virtual_account.deposit_funds_received | Funds have been received in the virtual account |
virtual_account.deposit_payment_submitted | Crypto payment submitted to blockchain (crypto mode) |
virtual_account.deposit_payment_processed | Crypto payment confirmed on blockchain (crypto mode) |
payout.pending | Payout created and awaiting processing |
payout.processing | Payout is being routed to banking partner |
payout.completed | Payout successfully sent to recipient |
payout.failed | Payout failed |
payout.returned | Funds returned after being sent |
liquidation.deposit_received | Crypto received at liquidation address |
liquidation.payout_processing | USD payout being processed |
liquidation.payout_completed | USD successfully sent to recipient |
liquidation.payout_failed | USD 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,
destinationisnull. For crypto mode accounts,destinationcontains 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
| Field | Description |
|---|---|
payment_rail | How funds were sent: ach_push, wire, spei |
sender_name | Name of the sender (if available) |
sender_bank_routing_number | Sender's bank routing number (US) |
trace_number | ACH trace number for tracking |
description | Payment 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
| Reason | Description |
|---|---|
compliance_rejected | Transaction did not pass compliance review |
insufficient_balance | Account balance became insufficient |
invalid_recipient | Recipient account details are invalid |
recipient_bank_rejected | Recipient's bank rejected the transfer |
compliance_hold | Transaction 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
| Reason | Description |
|---|---|
recipient_account_closed | Recipient's bank account is closed |
recipient_bank_rejected | Recipient's bank rejected the transfer |
incorrect_account_details | Account details don't match |
insufficient_funds_at_recipient | Recipient 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
- Always return 200 - Acknowledge receipt immediately, process asynchronously
- Handle duplicates - Use
event_idorpayout_idto detect duplicate deliveries - Process idempotently - Ensure handling the same event twice doesn't cause issues
- Log all events - Keep records for debugging and reconciliation
- Set up monitoring - Alert on failed webhook deliveries or processing errors
- Handle all event types - Even if you only care about some, log unknown types
- 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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
After 6 failed attempts, the webhook is marked as failed and no further retries are made.
Related Documentation
- Virtual Accounts Guide - Create and manage virtual accounts
- Deposits Guide - Track incoming deposits
- Payouts Guide - Initiate payouts from fiat mode accounts
- Liquidation Address Guide - Crypto-to-USD off-ramping
- Webhooks Setup - Configure webhook endpoints and signature verification
Updated 7 days ago
