GuidesAPI ReferenceDiscussions
Log In
Guides

Webhooks

Receive real-time notifications when important events happen during the loan lifecycle. Get instant updates on application status, document signing, funding progress, and more.

Webhooks

Webhooks let you receive automatic notifications when important events happen in Thrive. Instead of repeatedly checking for updates, we'll send the information directly to your system the moment something changes.

Getting Started with Webhooks

Webhooks are configured by the Thrive team. Contact your Thrive
representative with your endpoint URL, the events you want to receive, and
your signing secret if you are using HMAC or Bearer authentication.

What You'll Receive

When an event occurs, Thrive sends an HTTP POST request to your endpoint with a JSON payload containing the event details.

Every request is sent with Content-Type: application/json, plus an
authentication header if your subscription is configured for one (see
Securing Your Endpoint).

All payloads include:

FieldDescription
application_idThe unique identifier for the loan application
statusThe current status or state change
timestampWhen the event occurred (ISO 8601 format)

Additional fields are included depending on the event type.


Available Events

Application Events

These events notify you when a loan application's status or terms change.

application.status.update

Sent when the application status changes (e.g., Approved, Declined, Pending Review).

{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "status": "Approved",
  "application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f",
  "data": {
    "approved_offer": {
      "term": "60",
      "interest_rate": "8.99",
      "borrower_max_approved_amount": "25000.00",
      "project_max_approved_amount": "30000.00",
      "max_amount": "25000.00",
      "approved_amount": "25000.00",
      "merchant_fee_percentage": "7.00"
    }
  }
}

Offer Fields Explained:

FieldDescription
termLoan term in months
interest_rateAnnual interest rate (percentage)
borrower_max_approved_amountMaximum amount the borrower qualifies for
project_max_approved_amountMaximum amount allowed for the project
max_amountThe lesser of borrower and project maximums
approved_amountThe actual approved loan amount
merchant_fee_percentageYour merchant fee as a percentage

max_amount is present on application.status.update and
application.amount.update, and absent on application.offer.update. Every
other field above appears on all three application events.


application.amount.update

Sent when the approved loan amount changes after initial approval.

The status is always the literal string Loan Amount Updated. This event
carries previous_approved_offer and updated_approved_offer — the same shape
as application.offer.update, not the approved_offer shape used by
application.status.update.

{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "status": "Loan Amount Updated",
  "application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f",
  "data": {
    "previous_approved_offer": {
      "term": "60",
      "interest_rate": "8.99",
      "borrower_max_approved_amount": "20000.00",
      "project_max_approved_amount": "25000.00",
      "max_amount": "20000.00",
      "approved_amount": "20000.00",
      "merchant_fee_percentage": "7.00"
    },
    "updated_approved_offer": {
      "term": "60",
      "interest_rate": "8.99",
      "borrower_max_approved_amount": "25000.00",
      "project_max_approved_amount": "30000.00",
      "max_amount": "25000.00",
      "approved_amount": "25000.00",
      "merchant_fee_percentage": "7.00"
    }
  }
}

application.offer.update

Sent when loan terms change (rate, term, or fees) after a hard credit pull. This payload includes both the previous and updated offer so you can see exactly what changed.

{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "status": "Terms Updated",
  "application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f",
  "data": {
    "previous_approved_offer": {
      "term": "60",
      "interest_rate": "9.99",
      "borrower_max_approved_amount": "20000.00",
      "project_max_approved_amount": "25000.00",
      "approved_amount": "20000.00",
      "merchant_fee_percentage": "7.00"
    },
    "updated_approved_offer": {
      "term": "60",
      "interest_rate": "8.99",
      "borrower_max_approved_amount": "25000.00",
      "project_max_approved_amount": "30000.00",
      "approved_amount": "25000.00",
      "merchant_fee_percentage": "6.50"
    }
  }
}

max_amount is not sent on this event. Unlike
application.status.update and application.amount.update, the offer objects
on application.offer.update omit max_amount entirely. Treat it as optional
when parsing this event.


Document Events

These events track the loan document signing process.

document.sent

Sent when loan documents are delivered to the borrower for e-signature.

{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "status": "DOCUMENT_SENT",
  "application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f"
}

document.signed

Sent when the borrower completes signing all required loan documents.

{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "status": "DOCUMENT_SIGNED",
  "application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f"
}

Disbursement Events

These events track the funding process from request to settlement.

disbursement.funding.status-changed

Sent when the funding status changes. You'll receive this event when funding is requested and again when it begins processing.

{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "status": "FUNDING_REQUESTED",
  "application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f"
}

Possible Status Values:

StatusMeaning
FUNDING_REQUESTEDA funding request has been submitted
FUNDING_PROCESSINGThe funding request is being processed

disbursement.funding.settled

Sent when funds have been successfully transferred.

{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "status": "FUNDING_SETTLED",
  "application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f"
}

Task Events

These events notify you when tasks are created or updated on loan applications.

task.created

Sent when a new task is added to an application (e.g., document upload required).

{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "status": "Pending",
  "application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f",
  "task_title": "Upload proof of income"
}

task.updated

Sent when a task's status changes (e.g., completed, cancelled).

{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "status": "Completed",
  "application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f",
  "task_title": "Upload proof of income"
}

Setting Up Your Endpoint

Endpoint Requirements

Your webhook endpoint must meet these requirements:

  • HTTPS required - Your webhook URL must use HTTPS. Credentials (HMAC signature, Bearer token) travel in request headers, so an HTTP endpoint would expose them in transit.
  • Publicly accessible - Must be reachable from the internet
  • Responds quickly - Return a response within 30 seconds
  • Returns 2xx status - Any 2xx status code confirms successful receipt

Choosing Your Events

When setting up webhooks with your Thrive representative, you can choose to receive:

  • Specific events - Only the events you need (e.g., just disbursement.funding.settled)
  • All events - Every event type listed above

Most integrations subscribe to these key events:

Use CaseRecommended Events
Track funding progressdisbursement.funding.status-changed, disbursement.funding.settled
Monitor full loan lifecycleapplication.status.update, document.signed, disbursement.funding.settled
Stay informed of everythingAll events

Handling Webhooks

Responding to Webhooks

Always return a 200 OK response as quickly as possible. This confirms you received the webhook. If you need to do additional processing, do it after sending the response.

app.post('/webhooks/thrive', (req, res) => {
  // Respond immediately
  res.status(200).json({ received: true });

  // Then process the webhook
  processWebhook(req.body);
});

Processing Different Event Types

You can identify the event type by examining the payload:

app.post('/webhooks/thrive', (req, res) => {
  const { status, application_id, data, task_title } = req.body;

  // Task events include task_title
  if (task_title) {
    console.log(`Task "${task_title}" is now: ${status}`);
  }
  // Document events have a DOCUMENT_ prefix
  else if (status.startsWith('DOCUMENT_')) {
    console.log(`Document status: ${status}`);
  }
  // Disbursement events have a FUNDING_ prefix
  else if (status.startsWith('FUNDING_')) {
    console.log(`Funding status: ${status}`);
  }
  // The two fixed-status application events
  else if (status === 'Loan Amount Updated') {
    console.log(`Amount changed to ${data.updated_approved_offer.approved_amount}`);
  }
  else if (status === 'Terms Updated') {
    // Same shape, but without max_amount
    console.log(`Terms changed to ${data.updated_approved_offer.interest_rate}%`);
  }
  // application.status.update carries approved_offer
  else if (data?.approved_offer) {
    console.log(`Application ${application_id} is now: ${status}`);
  }

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

Securing Your Endpoint

HMAC Signature Verification (Recommended)

If you choose HMAC authentication, we'll sign each webhook using a shared secret. The signature is included in the x-hmac-signature header.

To verify the signature, compute the HMAC-SHA256 hash of the raw request body using your secret, then compare it to the signature we sent:

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

app.post('/webhooks/thrive', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-hmac-signature'];

  if (!verifySignature(req.body.toString(), signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature valid - process the webhook
  const payload = JSON.parse(req.body);
  // ...
});
import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Bearer Token Authentication

Alternatively, we can include a Bearer token in the Authorization header. Your endpoint should verify this token matches the one provided during setup.

Custom Headers (API Keys)

If your endpoint authenticates with something other than an HMAC signature or a
Bearer token — most commonly an API key in a header of your own choosing — we
can send static headers with every delivery instead:

POST /your-webhook HTTP/1.1
Content-Type: application/json
x-api-key: your-api-key-value

Tell your Thrive representative the exact header name and value you need. A few
constraints worth knowing when you pick a name:

  • Header names are lowercased before sending, so X-Api-Key arrives as
    x-api-key. Match case-insensitively on your end.
  • host, content-length, connection and transfer-encoding cannot be set.
  • If you also use HMAC or Bearer authentication, those headers take precedence
    and cannot be overridden by a custom header.

Custom headers can be combined with HMAC or Bearer auth, or used on their own.


Handling Retries and Duplicates

Automatic Retries

If your endpoint doesn't respond with a 2xx status code (or times out), we'll redeliver the webhook. Each event is attempted a maximum of 3 times total — the initial delivery plus 2 retries. After the third failed attempt the event stops being retried and is not delivered again, so make sure your endpoint is reliable to avoid missing events.

Duplicate Handling

Because of retries, you may occasionally receive the same webhook more than once. To handle this, track which webhooks you've already processed using the combination of application_id, status, and timestamp:

const processed = new Set();

app.post('/webhooks/thrive', (req, res) => {
  const { application_id, status, timestamp } = req.body;
  const key = `${application_id}:${status}:${timestamp}`;

  if (processed.has(key)) {
    // Already handled this webhook
    return res.status(200).json({ received: true });
  }

  processed.add(key);

  // Process the webhook...

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

For production systems, store processed webhook keys in a database with an expiration time (e.g., 24 hours).

Make your processing idempotent as well. The timestamp is normally
identical across retries of the same event, which is what makes the key above
work. Rather than relying on the key alone, apply each update idempotently
(for example, setting an application's status rather than appending to a
history) so a duplicate delivery is harmless either way.


Testing Your Integration

Local Development

Use a tool like ngrok to expose your local development server:

ngrok http 3000

This gives you a public HTTPS URL that forwards to your local machine. Share this URL with your Thrive representative for testing.

Sample Test Payloads

Use these sample payloads to test your webhook handler:

Application Approved:

curl -X POST https://your-endpoint.com/webhooks/thrive \
  -H "Content-Type: application/json" \
  -d '{
    "timestamp": "2025-01-15T10:30:00.000Z",
    "status": "Approved",
    "application_id": "test-app-123",
    "data": {
      "approved_offer": {
        "term": "60",
        "interest_rate": "8.99",
        "borrower_max_approved_amount": "25000.00",
        "project_max_approved_amount": "30000.00",
        "max_amount": "25000.00",
        "approved_amount": "25000.00",
        "merchant_fee_percentage": "7.00"
      }
    }
  }'

Documents Signed:

curl -X POST https://your-endpoint.com/webhooks/thrive \
  -H "Content-Type: application/json" \
  -d '{
    "timestamp": "2025-01-15T10:30:00.000Z",
    "status": "DOCUMENT_SIGNED",
    "application_id": "test-app-123"
  }'

Funding Settled:

curl -X POST https://your-endpoint.com/webhooks/thrive \
  -H "Content-Type: application/json" \
  -d '{
    "timestamp": "2025-01-15T10:30:00.000Z",
    "status": "FUNDING_SETTLED",
    "application_id": "test-app-123"
  }'

Quick Reference

All Event Types

EventWhen It's Sent
application.status.updateApplication status changes (Approved, Declined, etc.)
application.amount.updateApproved loan amount is modified
application.offer.updateLoan terms change after hard pull
document.sentDocuments sent to borrower for signing
document.signedBorrower completes document signing
disbursement.funding.status-changedFunding requested or processing
disbursement.funding.settledFunds successfully transferred
task.createdNew task added to application
task.updatedTask status changes

All Status Values

CategoryPossible Values
Application (status.update)The application's current status name, e.g. Approved, Declined, Hard Pull
Application (amount.update)Always Loan Amount Updated
Application (offer.update)Always Terms Updated
DocumentDOCUMENT_SENT, DOCUMENT_SIGNED
FundingFUNDING_REQUESTED, FUNDING_PROCESSING, FUNDING_SETTLED
TaskThe task's current status, e.g. Pending, Completed

Only application.status.update carries a variable status drawn from the
application's workflow. The other two application events always send the fixed
strings above, which is the most reliable way to tell them apart.


Managing Webhook Subscriptions

Not yet available. Self-service webhook subscription management is not
released yet.

To set up or change a webhook subscription today, contact your Thrive
representative with your endpoint URL, the events you want, and your signing
secret. This page will be updated when self-service ships.


Need Help?

Contact your Thrive representative to:

  • Get assistance with webhook configuration
  • Troubleshoot delivery issues
  • Request test webhooks
  • Discuss custom integration requirements

We're here to help you integrate successfully.


Did this page help you?