Webhooks
Receive real-time event notifications via HTTP webhooks with HMAC-SHA256 signature verification.
Overview
LendWorks delivers webhook events to your configured endpoints when important actions occur. Events are signed with HMAC-SHA256 for verification and retried automatically on failure.
Setting Up Webhooks
- Navigate to Admin > Webhooks in the LendWorks dashboard
- Click Add Endpoint
- Enter your endpoint URL (must be HTTPS)
- Select the events you want to receive
- Copy the signing secret — you'll need it for verification
Verification
Every webhook includes signature headers for verification. Always verify signatures to ensure webhooks are from LendWorks and haven't been tampered with.
Using the SDK
npm install @lendworks/webhook-verifyimport { createVerifier } from '@lendworks/webhook-verify'
const verifier = createVerifier({
secret: process.env.WEBHOOK_SECRET,
})
// In your webhook handler
const event = verifier.verify(rawBody, {
'x-webhook-signature': req.headers['x-webhook-signature'],
'x-webhook-id': req.headers['x-webhook-id'],
'x-webhook-timestamp': req.headers['x-webhook-timestamp'],
})
console.log(event.event) // 'lead.created'
console.log(event.data) // { leadId: '...', orgId: '...' }Manual Verification
If you can't use the SDK, verify manually:
import crypto from 'node:crypto'
function verifyWebhook(
rawBody: string,
signature: string,
timestamp: string,
secret: string
): boolean {
// Reject old timestamps (>5 minute tolerance)
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - parseInt(timestamp)) > 300) {
return false
}
// Compute expected signature
const payload = `${timestamp}.${rawBody}`
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
)
}Express Example
import express from 'express'
import { createVerifier } from '@lendworks/webhook-verify'
const app = express()
const verifier = createVerifier({ secret: process.env.WEBHOOK_SECRET })
app.post(
'/webhooks/lendworks',
express.raw({ type: 'application/json' }),
(req, res) => {
try {
const event = verifier.verify(req.body, {
'x-webhook-signature': req.headers['x-webhook-signature'],
'x-webhook-id': req.headers['x-webhook-id'],
'x-webhook-timestamp': req.headers['x-webhook-timestamp'],
})
switch (event.event) {
case 'lead.created':
handleNewLead(event.data)
break
case 'lead.status_changed':
handleStageChange(event.data)
break
case 'deal.funded':
handleFundedDeal(event.data)
break
default:
console.log(`Unhandled event: ${event.event}`)
}
res.sendStatus(200)
} catch (error) {
console.error('Webhook verification failed:', error.message)
res.sendStatus(401)
}
}
)Next.js App Router Example
import { createVerifier } from '@lendworks/webhook-verify'
import { NextRequest, NextResponse } from 'next/server'
const verifier = createVerifier({ secret: process.env.WEBHOOK_SECRET! })
export async function POST(req: NextRequest) {
const rawBody = await req.text()
try {
const event = verifier.verify(rawBody, {
'x-webhook-signature': req.headers.get('x-webhook-signature')!,
'x-webhook-id': req.headers.get('x-webhook-id')!,
'x-webhook-timestamp': req.headers.get('x-webhook-timestamp')!,
})
// Process the event
console.log(`Received ${event.event}:`, event.data)
return NextResponse.json({ received: true })
} catch {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
}
}Retry Policy
Failed deliveries (non-2xx responses or timeouts) are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 1 hour |
After 5 failed attempts, the event is moved to a dead letter queue.
Circuit Breaker
If an endpoint accumulates 50 consecutive failures, the subscription is automatically disabled. You'll receive an email notification and can re-enable it from the dashboard after fixing your endpoint.
Event Types
See the full Webhook Events reference for all event types and payload schemas.
Common events:
| Event | Description |
|---|---|
lead.created | New lead submitted |
lead.status_changed | Lead moved to a new pipeline stage |
application.submitted | Application submitted to lender |
application.approved | Lender approved the application |
application.declined | Lender declined the application |
deal.funded | Deal marked as funded |
Event Catalog Endpoint
Browse the full catalog programmatically:
curl https://api.lend.works/v1/webhooks/catalogThis endpoint is public (no authentication required) and returns all event types with descriptions and example payloads.
Testing
Test your webhook endpoint from Admin > Webhooks in the dashboard. The test sends a sample event to your URL so you can verify:
- Your endpoint is reachable
- Signature verification works
- Your handler processes events correctly
Best Practices
- Always verify signatures — Never trust webhook payloads without verification
- Respond quickly — Return
200immediately, then process asynchronously. Webhook deliveries time out after 10 seconds. - Handle duplicates — Use the
X-Webhook-Idheader to deduplicate. The same event may be delivered more than once. - Use raw body for verification — Parse the body after verification. Parsing then re-serializing can change the payload and break signature checks.
- Monitor your endpoint — Check the delivery history in the dashboard to catch failures early