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
Register your endpoint yourself with a single call — see
Managing Webhook Subscriptions. If you
would rather we set it up, contact your Thrive representative with your
endpoint URL and the events you want to receive.
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., Pend Clearance, Approved, Rejected).
{
"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",
"estimated_payment_amount": "518.79"
}
}
}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 |
estimated_payment_amount | Estimated monthly payment for the offer |
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.
estimated_payment_amount is an estimate produced during underwriting, not a
contractual figure — always defer to the loan documents. It is empty on events
raised before underwriting has computed it, which in practice means the early
statuses (Customer Identification through Offer Generation).
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",
"estimated_payment_amount": "415.03"
},
"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",
"estimated_payment_amount": "518.79"
}
}
}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",
"estimated_payment_amount": "424.94"
},
"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",
"estimated_payment_amount": "518.79"
}
}
}
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 actually reached the merchant's account.
Expect a gap of several days after the draw request. Settlement reflects
money landing, not the request being accepted. You will receive
FUNDING_REQUESTEDwithin moments of a draw, then nothing until settlement
completes. That silence is normal -- it does not mean the draw failed, and
re-requesting will not speed it up.
{
"timestamp": "2025-01-15T10:30:00.000Z",
"status": "FUNDING_SETTLED",
"application_id": "c4ff1199-a135-4739-b110-91f7c1bf9d2f"
}Reading the funded amount. Disbursement events carry the status and the
application id, but no amount. To get the amount, call
GET /application/{loanApplicationUid}, which returns:
| Field | Description |
|---|---|
total_funded_amount | Total gross disbursed on the application |
total_net_funded_amount | The same, net of fees — what the merchant is paid |
disbursement_count | Number of disbursements included in the totals |
These include disbursements that have been requested but not yet settled, so
the figure moves as soon as a draw is made rather than days later when funds
land. Because pending disbursements count, the total can decrease if a
disbursement is subsequently cancelled.
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",
"comments": [
{
"id": "0f521d47-4e0f-4ccf-8fe8-1d16a2345bb8",
"message": "Income document verified and approved.",
"createdAt": "2025-01-15T10:29:00.000Z"
}
]
}About comments: a task accumulates comments over its lifetime, so this
array can contain more than one entry and grows as the task is worked. Each
comment has only id, message and createdAt — there is no read or seen
flag. If you poll for changes, compare createdAt against the last one you
processed to identify new comments. comments is only sent on task.updated;
task.created does not include it.
Setting Up Your Endpoint
Endpoint Requirements
Your webhook endpoint must meet these requirements:
- HTTPS required - Your webhook URL must use HTTPS. Credentials 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 |
Narrowing to Specific Statuses
application.status.update fires on every status change, which for a single
application can mean a dozen callbacks. If you only care about certain
transitions, an event name can be suffixed with a status:
application.status.update every status change
application.status.update.pendClearance only Pend Clearance
application.status.update.approved only Approved
application.status.update.rejected only Rejected
The suffix is the status in camelCase — Pend Clearance becomes
pendClearance, Hard Pull Authorization becomes hardPullAuthorization.
Matching is case-insensitive, so you do not have to guess our casing.
The unsuffixed name keeps its original meaning and still receives every status,
so existing subscriptions are unaffected. Scoped and unscoped entries can be
mixed; you receive an event if any entry matches.
| Status | Suffix |
|---|---|
Customer Identification | customerIdentification |
Credit Underwriting | creditUnderwriting |
Fraud Review | fraudReview |
Property Review | propertyReview |
Offer Generation | offerGeneration |
Pend Clearance | pendClearance |
Hard Pull Authorization | hardPullAuthorization |
Hard Pull | hardPull |
DocuSign | docusign |
Pre-Funding Check | preFundingCheck |
Funding | funding |
Approved | approved |
Rejected | rejected |
Error | error |
Fully Funded | fullyFunded |
Closed | closed |
Revised Docs Pending | revisedDocsPending |
Waterfall Product Selection | waterfallProductSelection |
Three of these are worth extra explanation:
| Status | What it means for you |
|---|---|
Revised Docs Pending | The approved amount or terms changed after the borrower already signed, so revised documents were sent and the borrower must sign again. Draw requests are rejected while an application sits here. |
Waterfall Product Selection | The application did not qualify under the product originally selected and an alternative is being applied. Once applied, the application returns to Offer Generation with different terms -- so an application can appear to move backwards. If your logic assumes statuses only advance, account for this. |
Pre-Funding Check | Defined in the workflow but not currently in use -- it has not been emitted in over 60 days. Do not build against it; we will confirm before it becomes active. |
The suffix is derived from the status rather than validated against a list, so
a typo does not error — the subscription simply never matches. If you scope a
subscription and stop receiving events, check the spelling first.
This works on any event that carries a status, not just application events.
Choosing Your Scope
Your webhooks are attached to a merchant group — the set of merchants that
belong to your organization. How events are routed within that group is up to
you, and there are two options.
One endpoint for the whole group (default)
Your endpoint receives events for every merchant in your group. This is the
right choice if you run a single system that handles all of your merchants, and
it is what you get unless you ask for something else.
One endpoint per merchant
Each endpoint receives events for only its own merchant. Choose this if you
run separate systems per brand or division — for example, four brands that each
have their own portal and their own signing secret.
Each subscription identifies its merchant by merchant_uid. Your Thrive
representative provides these — they are not available through the API. Ask
for the merchant UIDs in your group before you start, and keep them alongside
your other integration settings; they do not change.
Which one do you need?
Use this rule: if you have more than one endpoint, you need per-merchant
scoping.
The reason matters. Registering several endpoints on a group without
per-merchant scoping does not split traffic between them — every endpoint
receives every merchant's events. And because the webhook payload identifies
the application but not the merchant, there is no field you can filter
on to sort it out afterwards. One brand's applications would arrive at another
brand's system with no way to tell them apart.
The two modes cannot be mixed within one group. A group is served either by a
single group-wide endpoint or by one endpoint per merchant.
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)Custom Headers (API Keys, Bearer Tokens)
If your endpoint expects a fixed credential rather than a computed
signature — an API key, a bearer token, or any other static header — set
custom_headers and we will send them with every delivery:
{
"custom_headers": {
"x-api-key": "your-api-key-value",
"authorization": "Bearer your-token"
}
}Arriving as:
POST /your-webhook HTTP/1.1
Content-Type: application/json
x-api-key: your-api-key-value
authorization: Bearer your-tokenA few constraints when choosing header names:
- 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, the
x-hmac-signatureheader takes precedence and
cannot be overridden by a custom header.
Custom headers can be combined with HMAC, or used on their own.
Which should you use? HMAC is the stronger option and what we recommend:
the signature is recomputed for every request, so it proves the payload
reached you unmodified. A static header only proves the sender knows a
secret, and can be replayed if intercepted. Use custom headers when your
endpoint already expects a fixed credential.
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) | See the full list below |
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 | Not Done, In Progress, In Review, Done, Cancelled, Failed |
Application statuses. application.status.update carries the application's
current workflow status. The full set:
| Status | Status |
|---|---|
Customer Identification | Approved |
Credit Underwriting | Rejected |
Fraud Review | Error |
Property Review | Closed |
Offer Generation | Funding |
Pend Clearance | Fully Funded |
Hard Pull Authorization | Revised Docs Pending |
Hard Pull | Waterfall Product Selection |
DocuSign | Pre-Funding Check |
Statuses come from the underwriting workflow and are not a closed set — treat
an unrecognised value as informational rather than failing the delivery.
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
You can register and update your own webhook subscription through the API.
Authentication
Use the same credentials as the rest of the Thrive API:
| Header | Description |
|---|---|
x-thrive-client-id | Your API client ID |
x-thrive-client-secret | Your API client secret |
Your merchant group is determined by these credentials. You never send it, and
you can only ever read or change your own subscriptions.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /webhooks/subscriptions | List your subscriptions |
GET | /webhooks/subscriptions/{id} | Fetch one subscription |
PUT | /webhooks/subscriptions/{id} | Create or update a subscription |
The {id} is a name you choose — lowercase letters, digits and hyphens, 3 to 64
characters. Pick it on your first PUT; it identifies that subscription from
then on. Choose something you will recognise later, like main or
east-region.
The two GET routes return the same subscription data. The difference is that
listing returns everything you have and never errors on a missing id, while
fetching by id returns a 404 if it does not exist and a 403 if the id
belongs to someone else. Use the list to see what you have; use the single
fetch to check one subscription after changing it.
Creating or updating a subscription
curl -X PUT https://api.partnerwiththrive.com/public/webhooks/subscriptions/my-portal \
-H "x-thrive-client-id: $THRIVE_CLIENT_ID" \
-H "x-thrive-client-secret: $THRIVE_CLIENT_SECRET" \
-H "content-type: application/json" \
-d '{
"url": "https://your-portal.example.com/thrive-webhook",
"subscriptions": ["application.status.update", "document.signed"],
"auth_type": "hmac-sha256",
"secret": "your-signing-secret"
}'Fields
| Field | Required | Description |
|---|---|---|
url | Yes | Your HTTPS endpoint |
subscriptions | Yes | Event names you want — see All Event Types |
auth_type | No | none or hmac-sha256. Defaults to none |
secret | With auth | Your HMAC signing secret. Required when auth_type is hmac-sha256 |
custom_headers | No | Static headers, e.g. {"x-api-key": "..."} |
enabled | No | Defaults to true. Set false to pause delivery without deleting |
merchant_scoped | No | Set true to receive only one merchant's events — see Choosing Your Scope |
merchant_uid | With scope | The merchant this subscription serves, supplied by your Thrive representative. Required when merchant_scoped is true |
PUT replaces the whole subscription. Any optional field you leave out is
cleared, so send the complete object each time.
Responses
| Code | Meaning |
|---|---|
200 | Updated an existing subscription |
201 | Created a new subscription |
400 | Invalid request — check the message for the field |
401 | Missing or invalid credentials |
403 | That subscription or merchant belongs to someone else |
409 | Conflicts with a subscription you already have — see below |
A 409 means one of two things: you already have a subscription for that
merchant under a different id, or you are mixing group-wide and per-merchant
delivery in the same group. The response names the conflicting subscription.
Setting up several merchants
If you run a separate system per merchant, create one subscription for each.
There is no bulk call — send one PUT per merchant, each with its own id:
# First merchant
PUT /webhooks/subscriptions/east-region
{
"url": "https://east.example.com/thrive-webhook",
"subscriptions": ["application.status.update"],
"auth_type": "hmac-sha256",
"secret": "east-signing-secret",
"merchant_scoped": true,
"merchant_uid": "<east merchant uid>"
}
# Second merchant — different id, different merchant_uid
PUT /webhooks/subscriptions/west-region
{
"url": "https://west.example.com/thrive-webhook",
"subscriptions": ["application.status.update"],
"auth_type": "hmac-sha256",
"secret": "west-signing-secret",
"merchant_scoped": true,
"merchant_uid": "<west merchant uid>"
}Each merchant can appear in only one subscription. Reusing a merchant_uid
under a second id returns 409, because two subscriptions for the same merchant
would each receive a copy of every event.
Decide group-wide or per-merchant before you start. Once you have per-merchant
subscriptions you cannot add a group-wide one, and the reverse is also true.
Changing a subscription later
Everything is the same PUT to the same id.
| To do this | Send |
|---|---|
| Change your endpoint | the same id with a new url |
| Add or remove events | the same id with a revised subscriptions list |
| Rotate your secret | the same id with a new secret |
| Pause deliveries | the same id with "enabled": false |
| Resume | the same id with "enabled": true |
| Add another merchant | a new id with that merchant's merchant_uid |
PUTreplaces the whole subscription. Any optional field you leave out is
cleared — omitcustom_headersand they stop being sent. Fetch the
subscription first, change what you need, and send the complete object back.
This is deliberate: a partial update is an easy way to silently lose a header
and not notice for weeks.
There is no delete. Setting "enabled": false stops delivery while keeping the
subscription and its id, so you can turn it back on with a single call. If you
need one removed entirely, contact your Thrive representative.
Reading your configuration back
GET returns your subscriptions with credential values replaced by a
fingerprint:
{
"id": "my-portal",
"url": "https://your-portal.example.com/thrive-webhook",
"auth_type": "hmac-sha256",
"secret": "sha256:a7bc1d2e",
"enabled": true
}The fingerprint is stable, so you can confirm which secret is stored and notice
if it changes. The secret itself is never returned.
Not Receiving Webhooks?
If your subscription exists but nothing is arriving, these are the usual causes,
roughly in the order they turn out to be the problem.
The events are for a different merchant than you expect. This is the most
common one by far. Webhooks are routed by the merchant on the application, not
by who created it. If your test applications were submitted under a merchant
outside your group — or, when using per-merchant subscriptions, under a
different merchant than the one on that subscription — nothing will arrive and
nothing is broken. Check which merchant the application belongs to first.
Your subscription is paused. GET it and confirm "enabled": true.
Your firewall or WAF is blocking us. Deliveries come from Thrive's servers,
not from a browser, so anything that challenges unknown clients — bot
protection, IP filtering, geo rules — will reject them before your application
sees the request. Two things worth knowing:
- Testing with
curlfrom your own machine does not prove we can reach you.
Your laptop has a different IP, and a browser session may carry cookies that a
server-to-server call never has. - Ask your Thrive representative for our sending IP addresses so your security
team can allowlist them, and confirm the exception covers bot-protection
rules, not just IP filtering.
Your endpoint is not returning a 2xx. Anything else, or no response within
30 seconds, counts as a failure. Return 200 immediately and do your processing
afterwards.
You are not subscribed to that event. Check the subscriptions list on your
subscription against All Event Types. It is easy to expect
application.offer.update and find you only subscribed to
application.status.update.
If none of these explain it, contact your Thrive representative with your
subscription id and roughly when you expected the event. We can see delivery
attempts and the response your endpoint gave, which usually settles it quickly.
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 28 days ago
