Skip to main content

Webhooks

Receive real-time notifications when events happen in your ZendFi account. Webhooks let you automate workflows, update your systems, and provide instant feedback to users.

How Webhooks Work

Payment Event → ZendFi → POST to Your URL → Your Server Processes
  1. An event occurs (payment confirmed, subscription renewed, etc.)
  2. ZendFi sends an HTTP POST request to your webhook URL
  3. Your server receives and processes the event
  4. Respond with 200 OK to acknowledge receipt

Quick Setup

1. Create a Webhook Endpoint

Build an endpoint on your server to receive webhook events:

// Express.js example
import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

app.post('/webhooks/zendfi', (req, res) => {
// Verify the webhook signature
const signature = req.headers['x-zendfi-signature'];
const payload = JSON.stringify(req.body);

const expectedSignature = crypto
.createHmac('sha256', process.env.ZENDFI_WEBHOOK_SECRET)
.update(payload)
.digest('hex');

if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Process the event
const { event, payment } = req.body;

switch (event) {
case 'PaymentConfirmed':
// Grant access, send confirmation, update database
console.log('Payment confirmed:', payment.id);
break;
case 'SubscriptionRenewed':
// Extend subscription period
console.log('Subscription renewed:', payment.id);
break;
// ... handle other events
}

// Acknowledge receipt
res.status(200).json({ received: true });
});

app.listen(3000);

2. Configure Your Webhook URL

Set your webhook URL in the merchant dashboard or via API:

curl -X PUT https://api.zendfi.tech/api/v1/merchants/me/webhook \
-H "Authorization: Bearer zfi_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://myapp.com/webhooks/zendfi"
}'

Response:

{
"success": true,
"webhook_url": "https://myapp.com/webhooks/zendfi",
"webhook_secret": "whsec_abc123xyz789...",
"message": "Webhook URL updated successfully"
}
Save Your Secret!

The webhook secret is shown when you configure your webhook URL. Save it securely - you'll need it to verify webhook signatures.

3. Start Receiving Events

Once configured, ZendFi will send events to your URL immediately when they occur.

Webhook Events

Payment Events

EventDescriptionTrigger
PaymentCreatedPayment request createdCreate payment API called
PaymentConfirmedPayment confirmed on-chainTransaction confirmed
PaymentFailedPayment failedTransaction failed or expired
PaymentExpiredPayment link expiredExpiration time reached

Payment Intent Events

EventDescriptionTrigger
PaymentIntentCreatedPayment intent createdCreate intent API called
PaymentIntentRequiresPaymentAwaiting paymentIntent ready for payment
PaymentIntentSucceededIntent payment successfulPayment confirmed
PaymentIntentCanceledIntent cancelledCancel API called
PaymentIntentFailedIntent payment failedPayment failed

Subscription Events

EventDescriptionTrigger
SubscriptionCreatedNew subscription startedCustomer subscribed
SubscriptionRenewedSubscription billing completedRenewal payment confirmed
SubscriptionPaymentFailedRenewal payment failedPayment not received
SubscriptionCancelledSubscription cancelledCancel API called

Installment Events

EventDescriptionTrigger
InstallmentPaidInstallment payment receivedPayment confirmed
InstallmentLatePayment overdueGrace period ended
InstallmentDefaultedCustomer defaultedExcessive late payments
InstallmentPlanCompletedAll installments paidFinal payment confirmed

Invoice Events

EventDescriptionTrigger
InvoiceCreatedInvoice createdCreate invoice API called
InvoiceSentInvoice emailedSend API called
InvoicePaidInvoice fully paidBalance reaches zero
EventDescriptionTrigger
PaymentLinkCreatedPayment link createdCreate link API called
PaymentLinkUsedPayment via linkLink payment confirmed

Escrow Events

EventDescriptionTrigger
EscrowCreatedEscrow contract createdCreate escrow API called
EscrowFundedEscrow fundedFunds deposited
EscrowReleasedFunds released to recipientRelease API called
EscrowRefundedFunds returned to payerRefund API called
EscrowDisputedEscrow disputedDispute API called

Withdrawal Events

EventDescriptionTrigger
WithdrawalInitiatedWithdrawal requestedWithdraw API called
WithdrawalCompletedWithdrawal successfulTransaction confirmed
WithdrawalFailedWithdrawal failedTransaction failed

Settlement Events

EventDescriptionTrigger
SettlementCompletedSettlement processedFunds settled
SettlementFailedSettlement failedSettlement error

Webhook Payload Structure

All webhooks follow this structure:

{
"event": "PaymentConfirmed",
"timestamp": "2025-10-26T15:30:00Z",
"signature": "t=1698325200,v1=abc123...",
"payment": {
// Payment data (see below)
}
}

Top-level fields:

FieldTypeDescription
eventstringEvent type (e.g., PaymentConfirmed)
timestampstringISO 8601 timestamp
signaturestringHMAC signature for verification
paymentobjectPayment data (for payment events)
settlementobjectSettlement data (for settlement events)
withdrawalobjectWithdrawal data (for withdrawal events)

Payment Event Example

{
"event": "PaymentConfirmed",
"timestamp": "2025-10-26T15:30:00Z",
"signature": "t=1698325200,v1=abc123def456...",
"payment": {
"id": "pay_xyz789",
"merchant_id": "merch_abc123",
"amount_usd": 99.99,
"status": "confirmed",
"transaction_signature": "5K2Nz7J8H2...",
"customer_wallet": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"payment_token": "USDC",
"mode": "live",
"payment_url": "https://checkout.zendfi.tech/pay/pay_xyz789",
"description": null,
"created_at": "2025-10-26T15:28:00Z",
"expires_at": "2025-10-26T16:28:00Z",
"metadata": {
"user_id": "user_12345",
"plan": "pro"
},
"splits": [
{
"id": "split_123",
"recipient_wallet": "PartnerWallet...",
"recipient_name": "Partner",
"percentage": 10.0,
"amount_usd": 9.99,
"amount_crypto": 9.99,
"currency": "USDC",
"status": "completed",
"transaction_signature": "5K2Nz7J8H2...",
"settled_at": "2025-10-26T15:30:00Z",
"failure_reason": null,
"split_order": 0
}
]
},
"settlement": null,
"withdrawal": null
}

Verifying Webhooks

Always Verify Signatures

Never process webhooks without verifying the signature. Attackers could send fake events to your endpoint.

Signature Verification

ZendFi signs all webhooks with your webhook secret using HMAC-SHA256.

Headers sent with each webhook:

HeaderDescription
X-ZendFi-SignatureHMAC-SHA256 signature
X-ZendFi-EventEvent type
X-ZendFi-DeliveryWebhook delivery ID
X-ZendFi-AttemptDelivery attempt number (1-5)

Verification Code Examples

Node.js:

import crypto from 'crypto';

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}

// Usage
const rawBody = JSON.stringify(req.body);
const signature = req.headers['x-zendfi-signature'];
const isValid = verifyWebhook(
rawBody,
signature,
process.env.ZENDFI_WEBHOOK_SECRET
);

Python:

import hmac
import hashlib

def verify_webhook(payload: str, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()

return hmac.compare_digest(signature, expected)

# Usage
is_valid = verify_webhook(
request.get_data(as_text=True),
request.headers.get('X-ZendFi-Signature'),
os.environ['ZENDFI_WEBHOOK_SECRET']
)

Managing Webhooks

Update Webhook URL

curl -X PUT https://api.zendfi.tech/api/v1/merchants/me/webhook \
-H "Authorization: Bearer zfi_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://myapp.com/webhooks/v2/zendfi"
}'

List Webhook Events

Get recent webhook deliveries:

curl -X GET https://api.zendfi.tech/api/v1/webhooks \
-H "Authorization: Bearer zfi_live_abc123..."

Response:

[
{
"id": "wh_xyz789",
"payment_id": "pay_abc123",
"merchant_id": "merch_abc123",
"event_type": "PaymentConfirmed",
"payload": { /* full webhook payload */ },
"webhook_url": "https://myapp.com/webhooks/zendfi",
"status": "delivered",
"attempts": 1,
"last_attempt_at": "2025-10-26T15:30:01Z",
"next_retry_at": null,
"response_code": 200,
"response_body": "{\"received\":true}",
"created_at": "2025-10-26T15:30:00Z"
}
]

Retry Failed Webhook

Manually retry a failed webhook:

curl -X POST https://api.zendfi.tech/api/v1/webhooks/wh_xyz789/retry \
-H "Authorization: Bearer zfi_live_abc123..."

Testing Webhooks

Send Test Event

Trigger a test webhook to verify your endpoint:

curl -X POST https://api.zendfi.tech/api/v1/merchants/me/webhook/test \
-H "Authorization: Bearer zfi_live_abc123..."

Response:

{
"success": true,
"status_code": 200,
"response_time_ms": 145,
"response_body": "{\"received\":true}",
"message": "Webhook test successful!"
}

Using ngrok for Local Development

  1. Install ngrok: npm install -g ngrok
  2. Start your local server: npm run dev
  3. Expose it: ngrok http 3000
  4. Use the ngrok URL as your webhook URL
curl -X PUT https://api.zendfi.tech/api/v1/merchants/me/webhook \
-H "Authorization: Bearer zfi_test_abc123..." \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://abc123.ngrok.io/webhooks/zendfi"
}'

Retry Policy

If your endpoint doesn't respond with 2xx status, ZendFi retries:

AttemptDelay
1Immediate
21 minute
35 minutes
415 minutes
51 hour
624 hours

After 5 failed attempts, the webhook is marked as exhausted and moved to the dead letter queue.

Retry Headers

Retry attempts include the attempt number:

HeaderDescription
X-ZendFi-AttemptDelivery attempt number (1-5)
X-ZendFi-DeliveryUnique delivery ID

Best Practices

Endpoint Design

  1. Respond Quickly - Return 200 immediately, process async
  2. Idempotency - Handle duplicate events gracefully
  3. Logging - Log all received webhooks for debugging
  4. Error Handling - Catch exceptions, don't crash on bad data

Idempotent Processing

Use the delivery ID to prevent processing duplicates:

app.post('/webhooks/zendfi', async (req, res) => {
const deliveryId = req.headers['x-zendfi-delivery'];

// Check if already processed
const existing = await db.webhookEvents.findOne({ deliveryId });
if (existing) {
return res.status(200).json({ received: true, duplicate: true });
}

// Mark as processing
await db.webhookEvents.create({ deliveryId, status: 'processing' });

// Acknowledge immediately
res.status(200).json({ received: true });

// Process asynchronously
processWebhookAsync(req.body);
});

Security

  1. Verify Signatures - Always verify HMAC signatures
  2. Use HTTPS - Only use HTTPS webhook URLs
  3. Validate Data - Don't trust webhook data blindly
  4. Rate Limiting - Implement rate limiting on your endpoint

Webhook Status

Check webhook delivery status:

StatusDescription
pendingQueued for delivery
deliveredSuccessfully delivered (200 OK)
failedFailed delivery, will retry
exhaustedFailed after 5 attempts, in dead letter queue

Next Steps

Ask AI about the docs...