Referral delivery has a redirect leg and a postback leg
In referral delivery, an applicant who passes screening is redirected to your flow with parameters that identify the click. The applicant applies on your site, and later your server sends a postback with the outcome. The click ID joins the two legs. Because the S2S postback runs server to server, it does not depend on the applicant's browser.
- 1CYBERTECHApplicant passes screeningScreened against the lender's rules before the redirect.
- RedirectThrough the applicant's browser
- click_id
- ct_9f2a61c4
- sub_id
- srch_pl_07
- utm_source
- cybertech
- sig
- 5be0c1…e1a4
- 2LenderApplicant applies on the lender's siteThe lender stores the parameters and keeps the credit decision.
- 3LenderServer sends the outcomeOnce the status is final, never from the browser.
- PostbackOutcome, server to server
- click_id
- ct_9f2a61c4
- status
- approved
- event_time
- 2026-10-27T09:14:03Z
- 4CYBERTECHOutcome is recordedMatched to the click by click ID, then used to tune the checks.
CYBERTECH records each outcome, and returned outcomes help tune the checks over time. The integration page compares this mode with the direct API.
Parameter names, status values and the signature scheme here are illustrative. They are agreed during onboarding, before launch, and that agreement takes precedence.
Redirect parameters arrive in the query string
In a branded redirect, the applicant's browser makes an HTTP GET to your landing URL. UTM and sub-id values pass through unchanged. If you use a hosted portal instead, the handoff is agreed at onboarding.
| Parameter | What it carries | What to do with it |
|---|---|---|
click_id | The identifier of this click, and the join key for every later status. | Store it against the application and treat it as an opaque string: never parse, truncate or change its case. |
sub_id | The traffic sub-source behind the click. | Keep it on the application to compare outcomes by source. |
utm_source | Campaign attribution in UTM form. | Pass it to your analytics unchanged. |
sig | A signature over the signed parameters, which are agreed at onboarding. | Verify it before you trust the rest. |
Illustrative parameters for a referral redirect.
# 1 The applicant leaves with signed parameters
GET https://apply.lender.test/start
?click_id=ct_9f2a61c4
&sub_id=srch_pl_07
&utm_source=cybertech
&sig=5be0c1…e1a4
Store the parameters before the applicant starts
Your server sends the postback later, so persist what you need when the redirect lands.
- The raw query string as received, for debugging.
click_id,sub_idandutm_source, saved against your application record and kept until the last status you have agreed to send.- The result of the signature check and the arrival time in UTC.
Verify the signed parameters before you trust them
The signature lets you confirm that CYBERTECH issued the parameters and nobody edited them. A common design is a shared secret key: the sender computes a keyed hash over the parameters, and the receiver recomputes it and compares (RFC 2104 describes one construction, HMAC). Whether your setup uses a shared key, and its construction, parameter order and encoding, are agreed at onboarding.
- Capture the parameters as receivedRead the raw query string before your framework decodes it. If the signature covers the received characters, re-encoding changes what was signed: RFC 3986 treats percent-encodings that differ only in hexadecimal case as equivalent, but a signature does not. If your setup signs decoded values instead, step 2 governs.
- Rebuild the signed stringAssemble it as agreed: which parameters, in which order, with which separators. Compute the keyed hash with the key agreed for your setup, and keep that key out of front-end code.
- Compare without a timing leakUse a comparison designed to resist timing analysis instead of ==. In Python that is
hmac.compare_digest, which takes ASCII-only strings or bytes, so compare bytes, such ashmac.compare_digest(expected.encode(), received.encode()): a stray non-ASCII character insigthen fails the check instead of raising an error. - Decide what a mismatch meansDo not attribute the click or send postbacks for it. Log the request. Whether the applicant may continue is your decision.
A valid signature shows the parameters are unaltered, not that the link is new: a copy of a signed link verifies every time. Ask at onboarding whether the signed string includes a timestamp, and review any click_id that starts more than one application.
Send the outcomes you agreed at onboarding
Test the status that matches your billable event first, because it decides what is billed; pricing explains how that event is defined.
| Status | Meaning | Send when |
|---|---|---|
submitted | Application completed. | Your system accepts it. |
approved | Positive credit decision. | The decision is final. |
rejected | Application declined. | The decision is final, with a reason_code. |
disbursed | Funds released. | Disbursal is confirmed. |
Illustrative status names. The list, the exact values and any reversal status are agreed at onboarding.
A rejected postback carries a reason_code: a code from the rejection list agreed at onboarding, never free text. Pricing shows the kinds of reason a contract needs. A reversal works the same way. If a status is reversed inside the validation window, send the agreed reversal status with its reason_code for the same click_id. Whether a reversal sent after the window closes still applies is agreed in advance.
Send the postback from your server when a status changes in your system of record, not on a timer and not from the browser, and only once the status is final. Set event_time to the moment the status changed, not the send time, in UTC with a trailing Z and the same number of fractional digits, so the strings sort in time order (RFC 3339, section 5.1). Use HTTPS, and ask how postbacks are authenticated.
# 2 Your server confirms the outcome
GET {agreed postback URL}
?click_id=ct_9f2a61c4
&status=approved
&event_time=2026-10-27T09:14:03Z
# ← 200 OK
# status=rejected also sends &reason_code=, a code from the agreed list
Plan for responses, retries and duplicates
- A 2xx response means the postback was received and accepted (RFC 9110). Stop retrying on the first one.
- Retry after a timeout, a dropped connection, or a 500, 502, 503 or 504 response. Wait longer after each attempt and add a random delay, so retries do not arrive in bursts (Amazon Web Services). Honour any
Retry-Afterheader (RFC 9110, section 10.2.3). - Cap the attempts or the total time, then queue the unsent status and alert someone, so a billable outcome is replayed rather than lost.
- Do not resend a 4xx request unchanged: that class means the client seems to have erred (RFC 9110, section 15.5). The usual exceptions are 408, where the client may repeat the request (section 15.5.9), and 429, which signals rate limiting.
- Expect duplicates. A sender that loses the connection cannot know whether the first request was applied, so HTTP allows repeating an idempotent request even if the original succeeded (RFC 9110, section 9.2.2). A common design counts the first accepted
click_idandstatuspair and answers later copies with a success code, which ends your retries; agree the rule at onboarding. - Expect late postbacks. Statuses can arrive late or out of order, so
event_timecarries the moment of the status change. Agree how the receiving side treats them.
Test every status before launch
Agree with your CYBERTECH contact at onboarding how to test, then run the full sequence before going live.
- A redirect with a valid signature: your landing page stores every parameter in the table above.
- A redirect with one parameter edited: verification fails and the visit is not attributed.
- One postback per agreed status, with a
reason_codeonrejectedand on any reversal, each response recorded and checked. - The same postback twice: the result matches the duplicate rule you agreed, and your sender stops retrying on the first 2xx.
- Statuses out of order, for instance
disbursedbeforeapproved: the result matches the ordering rule you agreed. - A 503 forced on the first attempt from a stub endpoint of your own: your sender retries with a growing wait and stops after the first 2xx.
Keep direct identifiers out of postbacks
We recommend that postbacks carry identifiers and statuses only: click_id, status and event_time match an outcome to a click, plus a reason_code on a rejection or reversal. Leave out names, phone numbers, email addresses, government ID numbers and free-text decline reasons. Even this set can identify an applicant to a partner that links the click_id to a person, so which statuses you send depends on your notice, the borrower's consent and local law; pricing gives the India rule.
A postback is a URL, and a query string can be stored in web logs. MITRE records sensitive data in a query string as a weakness (CWE-598), and NIST calls minimising the use, collection and retention of personal data a basic privacy principle (SP 800-122).
Check these causes first when the handoff fails
| Symptom | Likely cause and fix |
|---|---|
click_id is missing on the landing page | A redirect in your chain, such as HTTP to HTTPS or a sign-in hop, dropped the query string, or the parameters sit after a #, which never reaches your server (RFC 9110). Make every hop pass the query on. |
| The signature never verifies | The query string was re-encoded before the check, the signed string differs from what you agreed, or the key is outdated. Check the raw query string and confirm the key with your CYBERTECH contact. |
| Postbacks get a 4xx response | Template fields never filled, a value that is not percent-encoded (RFC 3986), a status or reason_code that was not agreed, or a click_id altered in case or length. Send values exactly as stored. |
| A postback returns 200 but the outcome is missing | The sender may call a different URL from the agreed one or send an altered click_id. Log each request with its response and match it to the stored click. |
| Statuses appear in the wrong order | event_time holds the send time or is not in UTC. Take it from the status change in your system, with a trailing Z. |
With referral working, the anti-fraud page describes the checks that returned outcomes help tune. To start on your parameters and statuses, contact us.
Related guides
- Loan leads reach you by referral or by direct APIReferral with a server-to-server postback and a direct API deliver the same screened applicants; this page helps you choose and walks through a launch.
- Receive loan leads by direct API with idempotent requestsLenders' engineers map fields, handle idempotency keys, retries and webhooks, and test in a sandbox before applications go live.
- How CPL, CPA and CPD price loan leadsHow CPL, CPA and CPD differ on risk and reporting, and which terms to fix in writing: billable event, validation window, caps.
- Screen loan leads for fraud and fit before they reach youFive checks, including fit to your own criteria, screen applicants before handoff. No single signal decides, and a stopped applicant is not delivered to you.
Sources
- IETF: RFC 2104, HMAC: Keyed-Hashing for Message Authentication (1997)
- IETF: RFC 9110, HTTP Semantics (2022)
- IETF: RFC 6585, Additional HTTP Status Codes (2012)
- IETF: RFC 3339, Date and Time on the Internet: Timestamps (2002)
- IETF: RFC 3986, Uniform Resource Identifier (URI): Generic Syntax (2005)
- MITRE: CWE-598, Use of HTTP Request With Sensitive Query String (2026)
- NIST: SP 800-122, Guide to Protecting the Confidentiality of Personally Identifiable Information (2010)
- Python Software Foundation: hmac, Keyed-Hashing for Message Authentication (Python 3 documentation, 2026)
- Amazon Web Services: Timeouts, retries, and backoff with jitter (2019)