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

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:

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., 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:

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
estimated_payment_amountEstimated 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

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",
      "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

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",
      "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_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 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_REQUESTED within 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:

FieldDescription
total_funded_amountTotal gross disbursed on the application
total_net_funded_amountThe same, net of fees — what the merchant is paid
disbursement_countNumber 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

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",
  "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 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

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.

StatusSuffix
Customer IdentificationcustomerIdentification
Credit UnderwritingcreditUnderwriting
Fraud ReviewfraudReview
Property ReviewpropertyReview
Offer GenerationofferGeneration
Pend ClearancependClearance
Hard Pull AuthorizationhardPullAuthorization
Hard PullhardPull
DocuSigndocusign
Pre-Funding CheckpreFundingCheck
Fundingfunding
Approvedapproved
Rejectedrejected
Errorerror
Fully FundedfullyFunded
Closedclosed
Revised Docs PendingrevisedDocsPending
Waterfall Product SelectionwaterfallProductSelection

Three of these are worth extra explanation:

StatusWhat it means for you
Revised Docs PendingThe 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 SelectionThe 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 CheckDefined 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-token

A few constraints when choosing header names:

  • 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, the x-hmac-signature header 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 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)See the full list below
Application (amount.update)Always Loan Amount Updated
Application (offer.update)Always Terms Updated
DocumentDOCUMENT_SENT, DOCUMENT_SIGNED
FundingFUNDING_REQUESTED, FUNDING_PROCESSING, FUNDING_SETTLED
TaskNot Done, In Progress, In Review, Done, Cancelled, Failed

Application statuses. application.status.update carries the application's
current workflow status. The full set:

StatusStatus
Customer IdentificationApproved
Credit UnderwritingRejected
Fraud ReviewError
Property ReviewClosed
Offer GenerationFunding
Pend ClearanceFully Funded
Hard Pull AuthorizationRevised Docs Pending
Hard PullWaterfall Product Selection
DocuSignPre-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:

HeaderDescription
x-thrive-client-idYour API client ID
x-thrive-client-secretYour 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

MethodPathDescription
GET/webhooks/subscriptionsList 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

FieldRequiredDescription
urlYesYour HTTPS endpoint
subscriptionsYesEvent names you want — see All Event Types
auth_typeNonone or hmac-sha256. Defaults to none
secretWith authYour HMAC signing secret. Required when auth_type is hmac-sha256
custom_headersNoStatic headers, e.g. {"x-api-key": "..."}
enabledNoDefaults to true. Set false to pause delivery without deleting
merchant_scopedNoSet true to receive only one merchant's events — see Choosing Your Scope
merchant_uidWith scopeThe 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

CodeMeaning
200Updated an existing subscription
201Created a new subscription
400Invalid request — check the message for the field
401Missing or invalid credentials
403That subscription or merchant belongs to someone else
409Conflicts 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 thisSend
Change your endpointthe same id with a new url
Add or remove eventsthe same id with a revised subscriptions list
Rotate your secretthe same id with a new secret
Pause deliveriesthe same id with "enabled": false
Resumethe same id with "enabled": true
Add another merchanta new id with that merchant's merchant_uid

PUT replaces the whole subscription. Any optional field you leave out is
cleared — omit custom_headers and 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 curl from 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.


Did this page help you?