LendWorksLendWorksDocs

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

  1. Navigate to Admin > Webhooks in the LendWorks dashboard
  2. Click Add Endpoint
  3. Enter your endpoint URL (must be HTTPS)
  4. Select the events you want to receive
  5. 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-verify
import { 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:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
51 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:

EventDescription
lead.createdNew lead submitted
lead.status_changedLead moved to a new pipeline stage
application.submittedApplication submitted to lender
application.approvedLender approved the application
application.declinedLender declined the application
deal.fundedDeal marked as funded

Event Catalog Endpoint

Browse the full catalog programmatically:

curl https://api.lend.works/v1/webhooks/catalog

This 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 200 immediately, then process asynchronously. Webhook deliveries time out after 10 seconds.
  • Handle duplicates — Use the X-Webhook-Id header 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