Status postbacks
For: Lenders (integration team)
Overview
Section titled “Overview”When a consumer applies for one of your products through Prosperas, you process that application on your side (approve it, disburse it, reject it). A postback is how you tell Prosperas what happened, in near real time, so we can update the lead status, reconcile revenue, and keep the consumer experience in sync.
The integration is deliberately small: one endpoint, one credential, one payload shape.
| Property | Value |
|---|---|
| Direction | You → Prosperas (you call us) |
| Endpoint | POST {BASE_URL}/client_app/webhook/lender/loan-status (an equivalent GET variant exists) |
| Auth | X-API-KEY header (recommended) or source-IP allowlist |
| Body format | JSON, snake_case |
| Events | application, approval, disbursement, rejected, no_payment_yet, on_time, late, default, paid_in_full |
| Success | 200 OK with a JSON acknowledgement |
| Retries | You retry on 5xx / network errors (we do not retry you) |
Authentication
Section titled “Authentication”Every postback must authenticate. Prosperas checks the API key first, then the source IP. If neither matches a registered credential, the request is rejected with 403.
API key (recommended)
Section titled “API key (recommended)”Send your key in the HTTP header:
X-API-KEY: <your-key>- The header name is case-insensitive (
X-API-KEY,x-api-key,X-Api-Keyall work). - The key is matched by exact string. No prefix, no
Bearer, no encoding.
IP allowlist (alternative)
Section titled “IP allowlist (alternative)”If you were provisioned by IP, no header is required — we read your source IP from X-Forwarded-For, then X-Real-IP, then the socket peer.
- The match is exact string against the IP we registered.
- No CIDR / subnet ranges are supported. Every individual egress IP you use must be registered. If your infrastructure rotates IPs, use the API key method instead.
The “a lead must exist” rule
Section titled “The “a lead must exist” rule”Authentication is not only about the credential — it is bound to a real lead. A request succeeds only when all of the following hold:
- Your credential (key or IP) matches a registered lender origin, and
- the
click_idin the request resolves to a lead, and - that lead belongs to a product owned by your lender account.
If your credential is valid but the click_id does not resolve to one of your leads, you still get 403 (not 404). In practice this means: a wrong, unknown, or foreign click_id returns 403.
The tracking id (click_id)
Section titled “The tracking id (click_id)”click_id is the single identifier that ties a consumer’s journey together across Prosperas and your systems.
- Prosperas generates it and hands it to you at the moment the consumer is redirected to you (as the tracking / click identifier in the hand-off URL).
- You must store it and echo it back — exactly — in every postback for that application.
- It carries no personal data; it is an opaque reference.
Accepted formats:
| Format | Example | Notes |
|---|---|---|
| Bare UUID | 7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e |
|
orgname_UUID |
claro_7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e |
Exactly one underscore |
More than one underscore, or an unparseable UUID, returns 400.
Request contract
Section titled “Request contract”Method: POST with a JSON body (recommended for server-to-server integrations). If your platform cannot issue a POST with a body, a fully supported GET variant takes the same fields as query parameters — same endpoint, same authentication, same processing.
Headers:
Content-Type: application/jsonX-API-KEY: <your-key>Body (JSON, snake_case):
{ "click_id": "claro_7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e", "event": "approval", "occurred_at": "2026-07-01T12:34:56Z", "data": { "provider": "your-brand", "client": { "email": "consumer@example.com", "fullName": "Ana Gomez", "firstName": "Ana", "lastName": "Gomez", "idNumber": "1234567890", "phoneNumber": "3001234567", "returningCustomer": false } }}Field reference
Section titled “Field reference”| Field | Type | Required | Notes |
|---|---|---|---|
click_id |
string | Yes | The tracking id described above. Echo it exactly. |
event |
string | Yes | One of the 9 supported events: application, approval, disbursement, rejected, no_payment_yet, on_time, late, default, paid_in_full (see Events & lifecycle below). Lowercase only — uppercase is rejected with 422. |
occurred_at |
string | — | ISO-8601 timestamp of when the event happened on your side (e.g. 2026-07-01T12:34:56Z). |
data |
object | — | Optional audit blob, free-form. Recognized keys: provider (string) and client (object below). Payment-status events additionally have recommended keys (see Payment status updates below); they are not required and are not validated. |
data.client |
object | — | Optional consumer data. See next section. |
The data.client object (optional)
Section titled “The data.client object (optional)”data.client lets you enrich the consumer record. All fields are optional.
| Field | Type | Notes |
|---|---|---|
email |
string | |
firstName |
string | Used only if fullName is absent. |
lastName |
string | Used only if fullName is absent. |
fullName |
string | Takes priority: first token → first name, remainder → last name. |
idNumber |
string | National ID / document number. |
phoneNumber |
string | |
returningCustomer |
boolean | Defaults to false if omitted. See the caution below. |
GET variant (query parameters)
Section titled “GET variant (query parameters)”If your platform cannot issue a POST with a JSON body — a tag manager, a legacy system, a rules engine that only knows how to call URLs — you can report the exact same event with a GET against the same endpoint, passing the fields as query parameters:
GET {BASE_URL}/client_app/webhook/lender/loan-status?click_id=<id>&event=<event>It goes through the same authentication, the same event → status mapping and the same persistence as the POST. It returns the same JSON acknowledgement (200) and the same error codes (400 / 403 / 404 / 422 / 5xx), with the same retry rules.
Query parameters
Section titled “Query parameters”| Parameter | Type | Required | POST equivalent |
|---|---|---|---|
click_id |
string | Yes | click_id |
event |
string | Yes | event (lowercase; one of the 9 events) |
occurred_at |
string | — | occurred_at (ISO-8601) |
api_key |
string | — | X-API-KEY header (see below) |
provider |
string | — | data.provider |
email |
string | — | data.client.email |
firstName |
string | — | data.client.firstName |
lastName |
string | — | data.client.lastName |
fullName |
string | — | data.client.fullName |
idNumber |
string | — | data.client.idNumber |
phoneNumber |
string | — | data.client.phoneNumber |
returningCustomer |
boolean | — | data.client.returningCustomer |
All values must be URL-encoded (emails, names with spaces, the + in an international phone number). Parameters not listed in this table are ignored and never stored.
Authenticating a GET
Section titled “Authenticating a GET”All three authentication methods still apply, in this order of preference:
X-API-KEYheader — recommended on theGETtoo. AGETcan carry headers; if your client can set them, do this.api_keyquery parameter — the fallback for clients that cannot set headers.- IP allowlist — no credential on the request, if your source IP is registered.
Example
Section titled “Example”curl -G "{BASE_URL}/client_app/webhook/lender/loan-status" \ -H "X-API-KEY: <your-key>" \ --data-urlencode "click_id=claro_7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e" \ --data-urlencode "event=approval" \ --data-urlencode "occurred_at=2026-07-01T12:34:56Z" \ --data-urlencode "provider=your-brand" \ --data-urlencode "email=consumer@example.com" \ --data-urlencode "fullName=Ana Gomez"{ "ok": true, "lead_id": 12345, "user_id": 678, "status": "APPROVAL" }Events & lifecycle
Section titled “Events & lifecycle”Send one postback per status transition, using the event that matches what happened:
event |
Meaning | Required? |
|---|---|---|
application |
The consumer’s application was received / started on your side. | Optional |
approval |
The application was approved. | Yes |
disbursement |
Funds were disbursed. | Yes |
rejected |
The application was rejected. | Yes |
no_payment_yet |
The loan was disbursed but the first scheduled due date hasn’t arrived yet. | Yes — required |
on_time |
Payment was received on/before the due date (or within the standard grace period). | Yes — required |
late |
Payment was received after due date + grace, below your institution’s default threshold. | Yes — required |
default |
The loan met your institution’s internal default criteria. | Yes — required |
paid_in_full |
All scheduled payments and the outstanding balance were satisfied; the loan closes in good standing. | Yes — required |
application is accepted but optional — send it if you have a meaningful “application received” milestone; otherwise the three terminal outcomes (approval, disbursement, rejected) are what matter most.
The five payment-status events (no_payment_yet, on_time, late, default, paid_in_full) differ from the four above: they are not a single milestone but ongoing updates for the life of the loan, reported after disbursement. They replace one another as the payment situation changes — no_payment_yet is the initial state and gets replaced by on_time, late, or default once the first due date passes. Reporting all five events is required for every lead, effective before August 31. Approval and disbursement data only say who qualified for credit; payment data closes the loop on who actually repays, which improves matchmaking quality for all lenders. See Payment status updates below for the per-status detail.
Send events in the order they occur. Prosperas tolerates events arriving out of order, but in-order delivery keeps the lead history clean.
Payment status updates
Section titled “Payment status updates”Unlike the four events above, which mark a single milestone in the application, payment status updates are an ongoing report: you send a new postback every time the consumer’s payment situation changes, for the life of the loan.
Per-status definitions
Section titled “Per-status definitions”event |
Definition |
|---|---|
no_payment_yet |
The loan was disbursed but the first scheduled due date hasn’t arrived yet. This is the initial, healthy state — it gets replaced by on_time, late, or default once that date passes. |
on_time |
Payment was received on/before the due date, or within your institution’s standard grace period. |
late |
Payment was received after due date + grace, but still below your institution’s default threshold. |
default |
The loan met your institution’s internal default criteria. |
paid_in_full |
All scheduled payments and the outstanding balance (principal, interest, fees) were satisfied. The loan closes in good standing. |
DPD buckets (late)
Section titled “DPD buckets (late)”For late, it’s recommended to report the days-past-due (DPD) bucket in data.dpd_bucket:
| Bucket | Range |
|---|---|
1-30 |
1 to 30 days past due |
31-60 |
31 to 60 days past due |
61-90 |
61 to 90 days past due |
The 90+ DPD convention (default)
Section titled “The 90+ DPD convention (default)”The most common industry criterion for default is 90 or more days past due. If your institution uses a different threshold, report it explicitly in data.default_threshold_dpd so it’s clear which convention you’re applying.
Recommended data keys per event
Section titled “Recommended data keys per event”| Event | Recommended keys in data |
|---|---|
no_payment_yet |
disbursed_amount |
on_time |
— |
late |
dpd_bucket |
default |
default_threshold_dpd |
paid_in_full |
paid_off_date |
Examples per event
Section titled “Examples per event”no_payment_yet
{ "click_id": "claro_7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e", "event": "no_payment_yet", "occurred_at": "2026-08-01T09:00:00Z", "data": { "disbursed_amount": 500000 }}on_time
{ "click_id": "claro_7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e", "event": "on_time", "occurred_at": "2026-09-01T09:00:00Z", "data": {}}late
{ "click_id": "claro_7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e", "event": "late", "occurred_at": "2026-09-15T09:00:00Z", "data": { "dpd_bucket": "1-30" }}default
{ "click_id": "claro_7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e", "event": "default", "occurred_at": "2026-12-01T09:00:00Z", "data": { "default_threshold_dpd": 90 }}paid_in_full
{ "click_id": "claro_7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e", "event": "paid_in_full", "occurred_at": "2027-06-01T09:00:00Z", "data": { "paid_off_date": "2027-06-01" }}Responses & status codes
Section titled “Responses & status codes”Success body (200 OK):
{ "ok": true, "lead_id": 12345, "status": "APPROVAL", "user_id": 6789}ok— alwaystrueon success.lead_id— the Prosperas lead the event was recorded against.status— the internal status your event mapped to (APPLICATION/APPROVAL/DISBURSEMENT/REJECTED/NO_PAYMENT_YET/ON_TIME/LATE/DEFAULT/PAID_IN_FULL).user_id— the consumer record id, when available.
Status codes:
| Code | Meaning | What you should do |
|---|---|---|
200 |
Event recorded. | Done. |
400 |
Malformed click_id (bad UUID or too many underscores). |
Fix the click_id; do not retry unchanged. |
403 |
Auth failed or the click_id doesn’t resolve to one of your leads. |
Check your key/IP and that you’re echoing the correct click_id. Do not blindly retry. |
404 |
Session not found (rare edge, after auth succeeds). | Verify the click_id; contact Prosperas if it persists. |
422 |
Validation error — e.g. camelCase clickId, uppercase event, wrong types. |
Fix the payload shape (see common mistakes below). Do not retry unchanged. |
5xx |
Transient Prosperas-side error. | Retry with exponential backoff (see below). |
Common mistakes (read this before you build)
Section titled “Common mistakes (read this before you build)”These four mistakes account for almost every failed first integration. Avoid them:
| ❌ Don’t | ✅ Do |
|---|---|
Use camelCase clickId in the body |
Use snake_case click_id — camelCase → 422 |
Send event: "APPROVAL" (uppercase) |
Send lowercase approval — uppercase → 422 |
| Assume the endpoint is public / no auth | Send X-API-KEY (or call from an allowlisted IP) — else 403 |
| Put the API key in a query string out of convenience | Send the key in the X-API-KEY header — a GET can carry it too. ?api_key= exists only as a last resort (see GET variant) |
Retries & delivery guarantees
Section titled “Retries & delivery guarantees”- You own retries. Prosperas does not retry you. On a
5xxor a network failure, retry the postback with exponential backoff (e.g. 1s, 2s, 4s, 8s…), up to a sensible ceiling. - Do not retry on
4xx. A400,403, or422means the request is wrong — fix it and send a corrected request instead of hammering the same payload. - The endpoint is not idempotent. Each accepted postback records a new event. If you retry after a request that actually succeeded (but whose response you missed), you may create a duplicate event. Keep retries bounded and prefer retrying only on confirmed failures.
- Every request is logged on our side (with your API key masked) for support and reconciliation.
Worked examples
Section titled “Worked examples”Happy path — approval (POST)
Section titled “Happy path — approval (POST)”curl -X POST "{BASE_URL}/client_app/webhook/lender/loan-status" \ -H "Content-Type: application/json" \ -H "X-API-KEY: <your-key>" \ -d '{ "click_id": "claro_7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e", "event": "approval", "occurred_at": "2026-07-01T12:34:56Z", "data": { "provider": "your-brand" } }'Response — 200 OK:
{ "ok": true, "lead_id": 12345, "status": "APPROVAL", "user_id": 6789 }Disbursement with consumer enrichment (POST)
Section titled “Disbursement with consumer enrichment (POST)”curl -X POST "{BASE_URL}/client_app/webhook/lender/loan-status" \ -H "Content-Type: application/json" \ -H "X-API-KEY: <your-key>" \ -d '{ "click_id": "7e1c2f1f-9b4d-4c1d-9f6c-2f3c1a2b4d5e", "event": "disbursement", "occurred_at": "2026-07-01T15:00:00Z", "data": { "provider": "your-brand", "client": { "fullName": "Ana Gomez", "idNumber": "1234567890" } } }'Error — camelCase field (422)
Section titled “Error — camelCase field (422)”# WRONG: "clickId" (camelCase) instead of "click_id"curl -X POST "{BASE_URL}/client_app/webhook/lender/loan-status" \ -H "Content-Type: application/json" \ -H "X-API-KEY: <your-key>" \ -d '{ "clickId": "7e1c2f1f-...", "event": "approval" }'# → 422 Unprocessable Entity (field "click_id" is required)Error — missing / wrong credential (403)
Section titled “Error — missing / wrong credential (403)”# No X-API-KEY header and IP not allowlisted → 403curl -X POST "{BASE_URL}/client_app/webhook/lender/loan-status" \ -H "Content-Type: application/json" \ -d '{ "click_id": "7e1c2f1f-...", "event": "approval" }'# → 403 ForbiddenQuick reference
Section titled “Quick reference”| Endpoint | POST {BASE_URL}/client_app/webhook/lender/loan-status |
| Bodyless variant | GET on the same path with the fields as query parameters — see GET variant |
| Auth header | X-API-KEY: <your-key> (case-insensitive) |
| Required body fields | click_id (snake_case), event (lowercase) |
| Events | application · approval · disbursement · rejected · no_payment_yet · on_time · late · default · paid_in_full |
| Optional body fields | occurred_at (ISO-8601), data, data.client |
| Success | 200 → { ok, lead_id, status, user_id } |
| Client errors | 400 bad click_id · 403 auth/lead · 404 session · 422 validation |
| Retry on | 5xx / network only, exponential backoff |
| Signing | None (no HMAC) — protect the API key |
Important notes
Section titled “Important notes”Next step: with postbacks working, close out the integration with Product data.
Last verified: 2026-09-15 · v1.3 · matches api/client_app/webhook/router.py