Connect
v2025-08-28Support

Webhooks

Webhooks provide real-time notifications about events in your Cleavr Connect integration. They're the most scalable way to stay updated on recovery progress, payments, and user activities.

Info

Scalability Note: Webhooks are more efficient than polling. They reduce API calls and provide instant updates.

Setup

1. Configure Webhook Endpoint

Register your webhook endpoint during platform setup:

javascript
const webhook = await cleavr.webhooks.create({
url: 'https://yourapp.com/webhooks/cleavr',
events: [
'receivable.*', // All receivable events
'user.onboarding.completed',
'payment.received'
],
description: 'Production webhook endpoint'
});
// Response
{
"id": "wh_a1b2c3d4e5f6",
"url": "https://yourapp.com/webhooks/cleavr",
"secret": "whsec_xxxxxxxxxxxxx", // Save this!
"events": ["receivable.*", "user.onboarding.completed", "payment.received"],
"created_at": "2025-08-28T10:00:00Z"
}

2. Verify Webhook Signatures

Always verify webhook signatures to ensure requests come from Cleavr:

javascript
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhooks/cleavr', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['cleavr-signature'];
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event
processWebhookEvent(event);
// Always respond quickly
res.status(200).send('OK');
});

Event Types

receivable.created

Fired when a new receivable is submitted.

json
{
"id": "evt_a1b2c3d4e5f6",
"type": "receivable.created",
"created_at": "2025-08-28T10:00:00Z",
"data": {
"receivable_id": "rec_xyz123",
"user_id": "usr_clv_abc456",
"amount": 5000.00,
"currency": "EUR",
"status": "pending",
"debtor": {
"name": "Debtor Corp",
"has_email": true,
"has_phone": true
}
}
}

receivable.sent_to_recovery

Recovery process has started.

json
{
"type": "receivable.sent_to_recovery",
"data": {
"receivable_id": "rec_xyz123",
"status": "in_process",
"recovery_started_at": "2025-08-28T10:30:00Z",
"assigned_agent": "agent_123",
"commission_rate": 15.90,
"recovery_page_url": "https://recovery.cleavr.fr/ABCD1234EFGH"
}
}

receivable.contact.attempted

Recovery action taken (email/phone).

json
{
"type": "receivable.contact.attempted",
"data": {
"receivable_id": "rec_xyz123",
"contact_type": "email", // or "phone", "sms", "letter"
"contact_number": 3, // Third attempt
"contact_level": 0, // Primary contact
"next_action": "phone_call",
"next_action_date": "2025-08-30T09:00:00Z"
}
}

receivable.payment.received

Payment received from debtor.

json
{
"type": "receivable.payment.received",
"data": {
"receivable_id": "rec_xyz123",
"payment_amount": 5000.00,
"payment_type": "full", // or "partial"
"remaining_amount": 0,
"commission_amount": 795.00,
"net_amount": 4205.00,
"payment_date": "2025-09-02T14:30:00Z",
"payment_intent_id": "pi_xxxxx"
}
}

receivable.payment.partial

Partial payment requiring platform decision.

json
{
"type": "receivable.payment.partial",
"data": {
"receivable_id": "rec_xyz123",
"original_amount": 5000.00,
"paid_amount": 3000.00,
"remaining_amount": 2000.00,
"action_required": "platform_decision",
"decision_deadline": "2025-09-04T23:59:59Z"
}
}

receivable.disputed

Debtor disputes the debt.

json
{
"type": "receivable.disputed",
"data": {
"receivable_id": "rec_xyz123",
"dispute_reason": "service_not_delivered",
"dispute_message": "We never received the products",
"evidence_requested": [
"delivery_confirmation",
"signed_contract"
],
"evidence_deadline": "2025-09-07T23:59:59Z",
"recovery_paused": true
}
}

receivable.evidence.submitted

Evidence provided for dispute.

json
{
"type": "receivable.evidence.submitted",
"data": {
"receivable_id": "rec_xyz123",
"evidence_count": 2,
"evidence_types": ["delivery_confirmation", "contract"],
"recovery_resumed": true
}
}

receivable.recovered

Successfully recovered (terminal state).

json
{
"type": "receivable.recovered",
"data": {
"receivable_id": "rec_xyz123",
"total_recovered": 5000.00,
"commission_charged": 795.00,
"net_received": 4205.00,
"recovery_duration_days": 15,
"payout_scheduled": "2025-09-04T00:00:00Z"
}
}

receivable.cancelled

Recovery cancelled (terminal state).

json
{
"type": "receivable.cancelled",
"data": {
"receivable_id": "rec_xyz123",
"cancellation_reason": "paid_directly",
"cancelled_by": "platform", // or "user", "cleavr"
"cancelled_at": "2025-09-02T10:00:00Z"
}
}

Webhook Payload Structure

All webhook events follow this structure:

json
{
"id": "evt_unique_id",
"type": "event.type.name",
"api_version": "2025-08-28",
"created_at": "2025-08-28T10:00:00Z",
"data": {
// Event-specific data
},
"metadata": {
"platform_id": "plat_live_xxx",
"environment": "production"
}
}

Handling Webhooks

Best Practices

javascript
class WebhookHandler {
async handleEvent(event) {
// 1. Acknowledge receipt immediately
this.acknowledge();
// 2. Verify this is a new event (idempotency)
if (await this.isDuplicate(event.id)) {
return;
}
// 3. Store event for replay capability
await this.storeEvent(event);
// 4. Process based on event type
switch (event.type) {
case 'receivable.payment.received':
await this.handlePaymentReceived(event.data);
break;
case 'receivable.disputed':
await this.handleDispute(event.data);
break;
case 'receivable.payment.partial':
await this.handlePartialPayment(event.data);
break;
// ... other events
}
// 5. Mark as processed
await this.markProcessed(event.id);
}
async handlePaymentReceived(data) {
// Update your database
await db.invoices.update({
where: { external_id: data.receivable_id },
data: {
status: 'paid',
paid_amount: data.payment_amount,
paid_at: data.payment_date
}
});
// Notify your user
await emailService.send({
to: user.email,
subject: 'Payment Received!',
template: 'payment_received',
data: data
});
}
async handlePartialPayment(data) {
// Decide whether to continue recovery
const decision = await this.evaluatePartialPayment(data);
// Send decision back to Cleavr
await cleavr.receivables.handlePartialPayment(data.receivable_id, {
action: decision.continue ? 'continue_recovery' : 'mark_complete',
notes: decision.reason
});
}
}

Retry Logic

Cleavr implements exponential backoff for failed webhook deliveries:

  1. Immediate retry
  2. After 1 minute
  3. After 5 minutes
  4. After 30 minutes
  5. After 2 hours
  6. After 6 hours
  7. After 24 hours

After 7 failed attempts, the webhook is marked as failed.

Manual Retry

Retrieve missed events:

javascript
// List recent events
const events = await cleavr.webhooks.listEvents({
since: '2025-08-28T00:00:00Z',
type: 'receivable.payment.received',
limit: 100
});
// Replay specific event
await cleavr.webhooks.replayEvent('evt_xyz123');

Testing Webhooks

Using Test Events

Trigger test events in development:

javascript
// Send test webhook
await cleavr.webhooks.sendTest({
type: 'receivable.payment.received',
endpoint_id: 'wh_xxx'
});

Local Development

Use tools like ngrok for local testing:

Command Line
# Expose local server
ngrok http 3000
# Register ngrok URL as webhook endpoint
# https://abc123.ngrok.io/webhooks/cleavr

Webhook CLI

Test webhooks via CLI:

Command Line
# Listen to events
cleavr webhooks listen --forward-to localhost:3000/webhooks/cleavr
# Trigger test event
cleavr webhooks trigger receivable.payment.received \
--receivable rec_test_123 \
--amount 1000

Monitoring

Webhook Health

Check webhook delivery status:

javascript
const health = await cleavr.webhooks.getHealth('wh_xxx');
// Response
{
"endpoint_id": "wh_xxx",
"url": "https://yourapp.com/webhooks/cleavr",
"status": "healthy",
"success_rate": 99.8,
"average_response_time_ms": 145,
"last_error": null,
"failed_attempts_24h": 2,
"successful_attempts_24h": 1847
}

Event Logs

Query webhook event history:

javascript
const logs = await cleavr.webhooks.getLogs({
endpoint_id: 'wh_xxx',
status: 'failed', // or 'success'
since: '2024-01-15T00:00:00Z'
});

Security

IP Whitelisting

Cleavr webhooks originate from these IPs:

Command Line
Production:
- 34.89.123.45
- 34.89.123.46
- 34.89.123.47
Test:
- 35.90.234.56

Request Headers

All webhook requests include:

http
POST /webhooks/cleavr HTTP/1.1
Content-Type: application/json
Cleavr-Signature: sha256=xxxxxxxxxxxxx
Cleavr-Event-Id: evt_a1b2c3d4e5f6
Cleavr-Timestamp: 1234567890
User-Agent: Cleavr-Webhooks/1.0

Webhook Secret Rotation

Rotate webhook secrets periodically:

javascript
// Generate new secret
const newWebhook = await cleavr.webhooks.rotateSecret('wh_xxx');
// Update your configuration
process.env.WEBHOOK_SECRET = newWebhook.secret;
// Old secret remains valid for 24 hours

Common Patterns

State Machine Updates

javascript
// Track receivable state changes
const stateMachine = {
pending: ['in_process', 'cancelled'],
in_process: ['payment_received', 'disputed', 'cancelled'],
payment_received: ['recovered'],
disputed: ['in_process', 'cancelled'],
recovered: [], // Terminal state
cancelled: [] // Terminal state
};
function handleStateChange(event) {
const currentState = getReceivableState(event.data.receivable_id);
const newState = event.data.status;
if (!stateMachine[currentState].includes(newState)) {
logWarning(`Invalid state transition: ${currentState} -> ${newState}`);
}
updateReceivableState(event.data.receivable_id, newState);
}

Aggregating Events

javascript
// Build recovery timeline
class RecoveryTimeline {
constructor(receivableId) {
this.receivableId = receivableId;
this.events = [];
}
addEvent(webhookEvent) {
this.events.push({
timestamp: webhookEvent.created_at,
type: webhookEvent.type,
data: webhookEvent.data
});
this.events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
}
getSummary() {
return {
total_contacts: this.events.filter(e => e.type === 'receivable.contact.attempted').length,
days_to_recovery: this.calculateDaysToRecovery(),
dispute_occurred: this.events.some(e => e.type === 'receivable.disputed')
};
}
}

Troubleshooting

Common Issues

IssueCauseSolution
Missing eventsEndpoint returned errorCheck webhook health, replay events
Duplicate eventsNetwork retryImplement idempotency with event.id
Out of order eventsAsync processingUse event.created_at for ordering
Signature verification failsWrong secretVerify using correct webhook secret
Slow webhook processingSynchronous processingProcess async, respond immediately

Debug Mode

Enable detailed webhook logging:

javascript
await cleavr.webhooks.update('wh_xxx', {
debug_mode: true, // Logs full request/response
debug_email: 'platform@yourcompany.com'
});

Next Steps

  1. Handle Errors
  2. Review API Reference
  3. Implement Monitoring