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 |
| Auth | X-API-KEY header (recommended) orsource-IP allowlist |
| Body format | JSON, snake_case |
| Events | application, approval,disbursement, rejected |
| Success | 200 OK with a JSON acknowledgement |
| Retries | You retry on 5xx / network errors (wedo not retry you) |
{BASE_URL}is the Prosperas base URL for your market
(Colombia / Mexico). Prosperas will give you the exact staging and
production URLs during onboarding.
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) #
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.
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 #
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.
Security note: There is no HMAC / request-signing on
this endpoint. Protect your API key like any other secret: store it in a
secrets manager, never commit it, never send it in plaintext, and ask
Prosperas to rotate it if it is ever exposed.
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 #
Method: POST (recommended). A
GET variant exists with the same fields as query
parameters, but POST with a JSON body is the supported server-to-server
method.
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 reference #
| Field | Type | Required | Notes |
|---|---|---|---|
click_id |
string | Yes | The tracking id described above. Echo it exactly. |
event |
string | Yes | One of application, approval,disbursement, rejected. Lowercaseonly — 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. Recognized keys: provider (string)and client (object below). |
data.client |
object | — | Optional consumer data. See next section. |
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 cautionbelow. |
Important — we never overwrite consumer-entered
data.data.clientonly fills fields that are
currently empty on the consumer’s record. Anything the
consumer already provided in the Prosperas flow wins. This protects data
the consumer explicitly entered.
Caution —
returningCustomeris not
sticky. If you omitreturningCustomeron a
postback, it is treated asfalse. If you rely on this flag,
send it explicitly on every postback where it should be
true.
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 |
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.
Send events in the order they occur. Prosperas
tolerates events arriving out of order, but in-order delivery keeps the
lead history clean.
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).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 manyunderscores). |
Fix the click_id; do not retryunchanged. |
403 |
Auth failed or the click_id doesn’tresolve to one of your leads. |
Check your key/IP and that you’re echoing the correctclick_id. Do not blindly retry. |
404 |
Session not found (rare edge, after auth succeeds). | Verify the click_id; contact Prosperas if itpersists. |
422 |
Validation error — e.g. camelCase clickId, uppercaseevent, 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) #
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 anallowlisted IP) — else 403 |
| Put the API key in a query string or expect the pixel to authenticate |
Send the key in the X-API-KEY headeron a POST |
Retries & delivery
guarantees #
- You own retries. Prosperas does
not retry you. On a5xxor 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, 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 #
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) #
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) #
# 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) #
# 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
Quick reference #
| Endpoint | POST {BASE_URL}/client_app/webhook/lender/loan-status |
| Auth header | X-API-KEY: <your-key> (case-insensitive) |
| Required body fields | click_id (snake_case), event(lowercase) |
| Events | application · approval ·disbursement · rejected |
| 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 #
Casing: top-level fields are
snake_case(click_id,occurred_at). The nesteddata.clientobject usescamelCase(fullName,idNumber,returningCustomer) — this is intentional, not a bug.
Response: you send
eventlowercase (approval); the response returnsstatusin UPPERCASE (APPROVAL). This is expected.
Idempotency & retries: the endpoint is not idempotent and Prosperas does not retry you. If a timeout leaves you without a response after we recorded the event, a retry creates a duplicate event. Recommendation: retry only on
5xxor network errors, with exponential backoff (1s, 2s, 4s…, max 3 attempts), and dedupe on your side byclick_id+event+occurred_at. (Note: your decision/submission API in the Embedded Flow MUST be idempotent by tracking ID — that direction we do retry.)
Last verified: 2026-07-01 · v1.1 · matches
Lender-Postback-Integration-Guide.md
