Webhooks Guide
Overview
Webhooks allow you to receive real-time notifications about events happening in your Kira integration. Instead of polling the API, events are pushed to your server immediately when they occur.
Register Your Webhook URL
Register your webhook endpoint with the Kira API:
Endpoint
POST /webhooks/register
Request
curl -X POST https://api.balampay.com/webhooks/register \
-H "Authorization: Bearer GENERATED_TOKEN" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://your-domain.com/webhooks/kira",
"secret": "your_webhook_secret",
"client_uuid": "your-client-uuid"
}'Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
webhook_url | string | Yes | Your HTTPS endpoint that will receive webhooks (must be HTTPS in production) |
secret | string | Yes | Secret key used to sign webhook payloads for verification |
client_uuid | string | Yes | Your unique client identifier |
Response
{
"message": "Webhook registered successfully"
}Webhook Structure
All webhooks follow the same base structure:
{
"event": "event.name",
"data": {
...
}
}| Field | Type | Description |
|---|---|---|
event | string | The event type (e.g., user.created, virtual_account.deposit_funds_received) |
data | object | Event-specific payload |
Your endpoint must return a 200 status code to acknowledge receipt. Non-2xx responses will be treated as a delivery failure.
Signature Verification
All webhooks include an x-signature-sha256 header containing an HMAC SHA256 signature computed with the secret you provided during registration. Always verify this signature before processing webhooks.
Verification Steps
- Extract the
x-signature-sha256header from the request - Compute the HMAC SHA256 of the JSON-stringified request body using your secret
- Compare the computed signature with the received signature using a timing-safe comparison
Example (Node.js)
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature) {
const secret = process.env.KIRA_WEBHOOK_SECRET;
const computedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computedSignature)
);
}Important: If you use middleware that parses the JSON body (e.g.,
express.json()), you mustJSON.stringify()the parsed body to verify the signature — do not use the raw buffer.
Event Catalog
User Events
| Event | Description |
|---|---|
user.created | User created successfully |
user.updated | User profile updated |
user.verification.accepted | Identity verification (KYC/KYB) approved |
user.verification.failed | Identity verification (KYC/KYB) rejected |
user.created
Sent when a new user is created via POST /v1/users.
{
"event": "user.created",
"data": {
"event_id": "evt_550e8400-e29b-41d4-a716-446655440001",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"type": "individual",
"email": "[email protected]",
"phone": "+1234567890",
"status": "unverified",
"verification_status": "not_started",
"verification_mode": "automatic",
"created_at": "2024-01-15T10:30:00Z",
"verification_triggered": true
}
}user.updated
Sent when a user is updated via PUT /v1/users/{userId}.
{
"event": "user.updated",
"data": {
"event_id": "evt_550e8400-e29b-41d4-a716-446655440002",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"type": "individual",
"email": "[email protected]",
"phone": "+1234567890",
"status": "verified",
"verification_status": "verified",
"updated_at": "2024-01-15T11:00:00Z",
"updated_fields": {},
"requires_reverification": false,
"verification_triggered": false
}
}user.verification.accepted
Sent when identity verification (KYC/KYB) is approved.
{
"event": "user.verification.accepted",
"data": {
"event_id": "evt_550e8400-e29b-41d4-a716-446655440003",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"id": "ver_550e8400-e29b-41d4-a716-446655440010",
"verification_status": "verified",
"kyc_type": "full"
}
}user.verification.failed
Sent when identity verification is rejected. Includes an array of reasons for the rejection.
{
"event": "user.verification.failed",
"data": {
"event_id": "evt_550e8400-e29b-41d4-a716-446655440004",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"id": "ver_550e8400-e29b-41d4-a716-446655440010",
"verification_status": "rejected",
"reasons": ["Document expired", "Address mismatch"]
}
}Payment Link Events
| Event | Description |
|---|---|
card_payment | Card payment completed on a payment link |
barcode_generated | Cash payment barcode generated or status updated |
card_payment
Sent when a card payment is completed on a payment link.
{
"event": "card_payment",
"data": {
"event_id": "evt_550e8400-e29b-41d4-a716-446655440020",
"payment_link_id": "550e8400-e29b-41d4-a716-446655440030",
"client_uuid": "550e8400-e29b-41d4-a716-446655440040",
"reference": "YOUR_INTERNAL_REFERENCE",
"status": "completed"
}
}barcode_generated
Sent when a cash payment barcode is generated or its status changes.
{
"event": "barcode_generated",
"data": {
"event_id": "evt_550e8400-e29b-41d4-a716-446655440021",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"recipient_id": "550e8400-e29b-41d4-a716-446655440050",
"base_amount": "100.00",
"base_currency": "USD",
"quote_currency": "MXN",
"cash_request_id": "550e8400-e29b-41d4-a716-446655440060",
"cash_request_status": "DEPOSITED",
"client_uuid": "550e8400-e29b-41d4-a716-446655440040",
"payment_link_id": "550e8400-e29b-41d4-a716-446655440030",
"referenceid": "YOUR_INTERNAL_REFERENCE",
"metadata": {
"quote_id": "550e8400-e29b-41d4-a716-446655440070",
"quote_amount": "1850.00",
"fixed_fee": "3.99",
"rate": "18.50"
}
}
}Virtual Account & Payout Events
For virtual account deposit events, payout events, and settlement events, see the Virtual Account Webhooks Guide.
Best Practices
- Return 200 quickly - Acknowledge receipt immediately, process the event asynchronously
- Verify signatures - Always validate webhook authenticity before processing
- Implement idempotency - Use unique identifiers from the
datapayload (e.g.,event_id) to prevent duplicate processing - Use HTTPS - Webhook URLs must use HTTPS in production
- Store secrets securely - Use environment variables, never commit secrets to code
Testing
Local Development with ngrok
# 1. Start your local server
npm run dev
# 2. In another terminal, start ngrok
ngrok http 3000
# 3. Register the ngrok URL as your webhook endpoint
curl -X POST https://api.balampay.com/webhooks/register \
-H "Authorization: Bearer GENERATED_TOKEN" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://abc123.ngrok.io/webhooks/kira",
"secret": "dev_secret_123",
"client_uuid": "YOUR_CLIENT_UUID"
}'Sandbox
In sandbox mode, user verification is automatically approved and webhooks are sent immediately for user creation and verification events.
Support
- Email: [email protected]
-
Updated 3 days ago
