Skip to content

Status postbacks

For: Lenders (integration team)

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)

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.

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-Key all work).
  • The key is matched by exact string. No prefix, no Bearer, no encoding.

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.

Authentication is not only about the credential — it is bound to a real lead. A request succeeds only when all of the following hold:

  1. Your credential (key or IP) matches a registered lender origin, and
  2. the click_id in the request resolves to a lead, and
  3. 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.

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.

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/json
X-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 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.

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.

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.

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.

All three authentication methods still apply, in this order of preference:

  1. X-API-KEY header — recommended on the GET too. A GET can carry headers; if your client can set them, do this.
  2. api_key query parameter — the fallback for clients that cannot set headers.
  3. IP allowlist — no credential on the request, if your source IP is registered.
Ventana de terminal
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" }

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.

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.

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.

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

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

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

Success body (200 OK):

{
"ok": true,
"lead_id": 12345,
"status": "APPROVAL",
"user_id": 6789
}
  • ok — always true on 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)
  • You own retries. Prosperas does not retry you. On a 5xx or 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. A 400, 403, or 422 means 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.
Ventana de terminal
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)”
Ventana de terminal
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" }
}
}'
Ventana de terminal
# 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)”
Ventana de terminal
# No X-API-KEY header and IP not allowlisted → 403
curl -X POST "{BASE_URL}/client_app/webhook/lender/loan-status" \
-H "Content-Type: application/json" \
-d '{ "click_id": "7e1c2f1f-...", "event": "approval" }'
# → 403 Forbidden
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

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