Documentation Index
Fetch the complete documentation index at: https://mintlify.com/blindpaylabs/blindpay-node/llms.txt
Use this file to discover all available pages before exploring further.
What are Webhooks?
Webhooks enable BlindPay to send real-time notifications to your application when events occur. Instead of polling the API to check if something changed, BlindPay proactively sends HTTP POST requests to your webhook endpoint whenever important events happen.
This is essential for:
- Tracking payout and payin status changes
- Receiving notifications when receivers complete KYC verification
- Getting notified when new bank accounts are added
- Monitoring partner fee transactions
- Tracking Terms of Service acceptances
How Webhooks Work
- You create a webhook endpoint (URL) in your application that can receive POST requests
- You register this endpoint with BlindPay and specify which events you want to receive
- When an event occurs (e.g., a payout completes), BlindPay sends a POST request to your endpoint
- Your application receives the event, verifies the signature, and processes it
- Your application responds with a 200 status code to acknowledge receipt
Available Webhook Events
BlindPay supports the following webhook events:
Receiver Events
receiver.new - A new receiver has been created
receiver.update - A receiver’s information has been updated (including KYC status changes)
Bank Account Events
bankAccount.new - A new bank account has been added to a receiver
Payout Events
payout.new - A new payout has been created
payout.update - A payout’s status has changed
payout.complete - A payout has been completed successfully
payout.partnerFee - A partner fee payout has been processed
Payin Events
payin.new - A new payin has been created
payin.update - A payin’s status has changed
payin.complete - A payin has been completed successfully
payin.partnerFee - A partner fee payin has been processed
Blockchain Wallet Events
blockchainWallet.new - A new blockchain wallet has been created
Terms of Service Events
tos.accept - A receiver has accepted Terms of Service
Creating a Webhook Endpoint
Register Your Endpoint
const { data, error } = await blindpay.instances.webhookEndpoints.create({
url: "https://your-app.com/api/blindpay/webhooks",
events: [
"payout.new",
"payout.update",
"payout.complete",
"receiver.update"
]
});
if (data) {
console.log("Webhook endpoint created:", data.id);
// Store this ID for future reference
}
List Your Webhook Endpoints
const { data, error } = await blindpay.instances.webhookEndpoints.list();
if (data) {
data.forEach(endpoint => {
console.log(`Endpoint ${endpoint.id}:`);
console.log(` URL: ${endpoint.url}`);
console.log(` Events: ${endpoint.events.join(", ")}`);
console.log(` Last event: ${endpoint.last_event_at}`);
});
}
Get Your Webhook Secret
Every webhook endpoint has a secret key used to sign webhook payloads:
const { data, error } = await blindpay.instances.webhookEndpoints.getSecret(
"we_abc123"
);
if (data) {
console.log("Webhook secret:", data.key);
// Store this securely - you'll need it to verify webhook signatures
}
Store your webhook secret securely! Never commit it to version control or expose it in client-side code. Treat it like a password.
Delete a Webhook Endpoint
const { data, error } = await blindpay.instances.webhookEndpoints.delete(
"we_abc123"
);
if (!error) {
console.log("Webhook endpoint deleted");
}
Implementing Your Webhook Handler
Node.js / Express Example
import express from 'express';
import { BlindPay } from 'blindpay-node-sdk';
const app = express();
const blindpay = new BlindPay({
apiKey: process.env.BLINDPAY_API_KEY!,
instanceId: process.env.BLINDPAY_INSTANCE_ID!
});
app.post('/api/blindpay/webhooks',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['svix-signature'] as string;
const id = req.headers['svix-id'] as string;
const timestamp = req.headers['svix-timestamp'] as string;
const body = req.body.toString();
// Verify the webhook signature
const isValid = blindpay.verifyWebhookSignature({
secret: process.env.BLINDPAY_WEBHOOK_SECRET!,
headers: {
id,
timestamp,
signature
},
payload: body
});
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse the event
const event = JSON.parse(body);
// Handle the event
try {
await handleWebhookEvent(event);
res.status(200).json({ received: true });
} catch (error) {
console.error('Error processing webhook:', error);
res.status(500).json({ error: 'Processing failed' });
}
}
);
const handleWebhookEvent = async (event: any) => {
switch (event.type) {
case 'payout.complete':
await handlePayoutComplete(event.data);
break;
case 'payout.update':
await handlePayoutUpdate(event.data);
break;
case 'receiver.update':
await handleReceiverUpdate(event.data);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
};
const handlePayoutComplete = async (payout: any) => {
console.log(`Payout ${payout.id} completed`);
// Update your database
// Send notification to user
// etc.
};
const handlePayoutUpdate = async (payout: any) => {
console.log(`Payout ${payout.id} status: ${payout.status}`);
// Update your database
};
const handleReceiverUpdate = async (receiver: any) => {
console.log(`Receiver ${receiver.id} KYC status: ${receiver.kyc_status}`);
// Notify user if KYC is approved/rejected
};
Next.js API Route Example
// app/api/blindpay/webhooks/route.ts
import { BlindPay } from 'blindpay-node-sdk';
import { NextRequest, NextResponse } from 'next/server';
const blindpay = new BlindPay({
apiKey: process.env.BLINDPAY_API_KEY!,
instanceId: process.env.BLINDPAY_INSTANCE_ID!
});
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('svix-signature')!;
const id = req.headers.get('svix-id')!;
const timestamp = req.headers.get('svix-timestamp')!;
// Verify signature
const isValid = blindpay.verifyWebhookSignature({
secret: process.env.BLINDPAY_WEBHOOK_SECRET!,
headers: { id, timestamp, signature },
payload: body
});
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
const event = JSON.parse(body);
// Handle the event
switch (event.type) {
case 'payout.complete':
// Your logic here
console.log('Payout completed:', event.data.id);
break;
case 'receiver.update':
// Your logic here
console.log('Receiver updated:', event.data.id);
break;
}
return NextResponse.json({ received: true });
}
Webhook Signature Verification
BlindPay signs all webhook payloads using Svix. Always verify signatures to ensure the webhook came from BlindPay:
const isValid = blindpay.verifyWebhookSignature({
secret: "your_webhook_secret",
headers: {
id: "msg_abc123",
timestamp: "1234567890",
signature: "v1,signature_here"
},
payload: "{\"type\":\"payout.complete\",\"data\":{...}}"
});
if (!isValid) {
throw new Error("Invalid webhook signature - possible tampering");
}
The verifyWebhookSignature method:
- Returns
true if the signature is valid
- Returns
false if the signature is invalid or verification fails
- Never throws exceptions
NEVER process a webhook without verifying its signature. Attackers could send fake webhooks to manipulate your application.
Webhook Event Payload Structure
Each webhook has a consistent structure:
type WebhookEvent = {
type: WebhookEvents;
data: any; // The resource that changed (payout, receiver, etc.)
created_at: string;
};
Example payout.complete event:
{
"type": "payout.complete",
"data": {
"id": "po_abc123",
"status": "completed",
"receiver_id": "re_xyz789",
"sender_amount": 1000,
"receiver_amount": 4850,
"currency": "BRL",
"tracking_complete": {
"step": "completed",
"status": "paid",
"transaction_hash": "0x123...",
"completed_at": "2024-01-15T10:30:00Z"
}
},
"created_at": "2024-01-15T10:30:00Z"
}
Webhook Best Practices
1. Respond Quickly
Your webhook handler should respond with 200 as quickly as possible. Process heavy workloads asynchronously:
app.post('/webhooks', async (req, res) => {
// Verify signature
const isValid = verifySignature(req);
if (!isValid) {
return res.status(401).send();
}
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
const event = req.body;
processWebhookAsync(event).catch(err => {
console.error('Async webhook processing failed:', err);
});
});
const processWebhookAsync = async (event: any) => {
// Heavy database operations
// External API calls
// Email notifications
// etc.
};
2. Handle Duplicate Events
BlindPay may send the same event multiple times. Make your webhook handler idempotent:
const handlePayoutComplete = async (payout: any) => {
// Check if already processed
const existing = await db.webhookEvents.findOne({
event_id: payout.id,
event_type: 'payout.complete'
});
if (existing) {
console.log('Event already processed, skipping');
return;
}
// Process the event
await db.payouts.update({ id: payout.id }, { status: 'completed' });
// Mark as processed
await db.webhookEvents.insert({
event_id: payout.id,
event_type: 'payout.complete',
processed_at: new Date()
});
};
3. Implement Retry Logic
If your webhook handler fails, BlindPay will retry with exponential backoff. Handle transient failures gracefully:
const handleWebhookEvent = async (event: any, retries = 3) => {
for (let i = 0; i < retries; i++) {
try {
await processEvent(event);
return; // Success
} catch (error) {
console.error(`Attempt ${i + 1} failed:`, error);
if (i === retries - 1) {
// Final attempt failed - log to error tracking
await logFailedWebhook(event, error);
throw error;
}
// Wait before retrying
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
}
}
};
4. Log All Webhook Events
Keep a record of all received webhooks for debugging and auditing:
app.post('/webhooks', async (req, res) => {
const event = req.body;
// Log the raw webhook
await db.webhookLogs.insert({
event_type: event.type,
payload: event,
received_at: new Date(),
headers: req.headers
});
// Process
await handleWebhookEvent(event);
res.status(200).json({ received: true });
});
5. Monitor Webhook Health
Track webhook processing to detect issues:
const monitorWebhook = async (event: any, startTime: number) => {
const duration = Date.now() - startTime;
// Track metrics
await metrics.record({
name: 'webhook_processing_time',
value: duration,
tags: { event_type: event.type }
});
// Alert on slow processing
if (duration > 5000) {
await alerts.send({
message: `Slow webhook processing: ${event.type} took ${duration}ms`
});
}
};
Webhook Portal
BlindPay provides a webhook management portal where you can view event logs and debug issues:
const { data, error } = await blindpay.instances.webhookEndpoints.getPortalAccessUrl();
if (data) {
console.log("Portal URL:", data.url);
// Open this URL to access the webhook portal
}
The portal shows:
- All webhook deliveries and their status
- Request and response details
- Retry attempts
- Delivery failures and reasons
Testing Webhooks Locally
Use tools like ngrok to expose your local server for webhook testing:
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js # Running on http://localhost:3000
# Expose it publicly
ngrok http 3000
# Forwarding https://abc123.ngrok.io -> http://localhost:3000
Then register the ngrok URL as your webhook endpoint:
await blindpay.instances.webhookEndpoints.create({
url: "https://abc123.ngrok.io/api/blindpay/webhooks",
events: ["payout.complete"]
});
Common Use Cases
Update Order Status
const handlePayoutComplete = async (payout: any) => {
// Find the order in your database
const order = await db.orders.findOne({
blindpay_payout_id: payout.id
});
if (order) {
// Update order status
await db.orders.update(
{ id: order.id },
{
status: 'paid',
paid_at: new Date(),
receiver_amount: payout.receiver_amount
}
);
// Send confirmation email
await emailService.send({
to: order.customer_email,
subject: 'Payment Completed',
template: 'payment-confirmed',
data: { order, payout }
});
}
};
Notify User of KYC Status
const handleReceiverUpdate = async (receiver: any) => {
// Check if KYC status changed
if (receiver.kyc_status === 'approved') {
await notificationService.send({
user_id: receiver.external_id,
title: 'Identity Verified',
message: 'Your identity has been verified. You can now receive payments.',
type: 'success'
});
} else if (receiver.kyc_status === 'rejected') {
await notificationService.send({
user_id: receiver.external_id,
title: 'Verification Failed',
message: 'We were unable to verify your identity. Please contact support.',
type: 'error'
});
}
};
Track Partner Fee Revenue
const handlePayoutPartnerFee = async (event: any) => {
await db.revenue.insert({
payout_id: event.data.id,
partner_fee_amount: event.data.partner_fee_amount,
transaction_hash: event.data.tracking_partner_fee?.transaction_hash,
received_at: new Date()
});
// Update revenue dashboard
await updateRevenueDashboard();
};
Troubleshooting
Webhooks Not Being Received
- Check that your endpoint URL is publicly accessible
- Verify your server is responding with 200 status codes
- Check your webhook endpoint configuration in BlindPay
- Review logs in the webhook portal
Signature Verification Failing
- Ensure you’re using the raw request body (not parsed JSON)
- Verify you’re using the correct webhook secret
- Check that headers are being passed correctly
- Ensure no middleware is modifying the request body
Missing Events
- Check that the event type is registered in your webhook endpoint
- Review the webhook portal for delivery attempts
- Check your server logs for processing errors
- Verify your endpoint is responding quickly (< 5 seconds)
For additional support, access the webhook portal to view detailed delivery logs and debugging information.