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:
| Field | Description |
|---|---|
application_id | The unique identifier for the loan application |
status | The current status or state change |
timestamp | When 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
application.status.updateSent 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:
| Field | Description |
|---|---|
term | Loan term in months |
interest_rate | Annual interest rate (percentage) |
borrower_max_approved_amount | Maximum amount the borrower qualifies for |
project_max_approved_amount | Maximum amount allowed for the project |
max_amount | The lesser of borrower and project maximums |
approved_amount | The actual approved loan amount |
merchant_fee_percentage | Your 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
application.amount.updateSent 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
application.offer.updateSent 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_amountis not sent on this event. Unlike
application.status.updateandapplication.amount.update, the offer objects
onapplication.offer.updateomitmax_amountentirely. Treat it as optional
when parsing this event.
Document Events
These events track the loan document signing process.
document.sent
document.sentSent 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
document.signedSent 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
disbursement.funding.status-changedSent 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:
| Status | Meaning |
|---|---|
FUNDING_REQUESTED | A funding request has been submitted |
FUNDING_PROCESSING | The funding request is being processed |
disbursement.funding.settled
disbursement.funding.settledSent 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
task.createdSent 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
task.updatedSent 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 Case | Recommended Events |
|---|---|
| Track funding progress | disbursement.funding.status-changed, disbursement.funding.settled |
| Monitor full loan lifecycle | application.status.update, document.signed, disbursement.funding.settled |
| Stay informed of everything | All 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-valueTell 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-Keyarrives as
x-api-key. Match case-insensitively on your end. host,content-length,connectionandtransfer-encodingcannot 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
timestampis 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 3000This 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
| Event | When It's Sent |
|---|---|
application.status.update | Application status changes (Approved, Declined, etc.) |
application.amount.update | Approved loan amount is modified |
application.offer.update | Loan terms change after hard pull |
document.sent | Documents sent to borrower for signing |
document.signed | Borrower completes document signing |
disbursement.funding.status-changed | Funding requested or processing |
disbursement.funding.settled | Funds successfully transferred |
task.created | New task added to application |
task.updated | Task status changes |
All Status Values
| Category | Possible 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 |
| Document | DOCUMENT_SENT, DOCUMENT_SIGNED |
| Funding | FUNDING_REQUESTED, FUNDING_PROCESSING, FUNDING_SETTLED |
| Task | The 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.
Updated 9 days ago
