Requesting Offers
Show the applicant the partners that currently qualify for them.
POST /api/v2/offer is the first step of the Comparisons integration. It validates the applicant, creates the lead, and returns the partners that currently qualify, so the applicant can choose one.
Nothing is sent to a partner until you accept an offer.
See POST /api/v2/offer in the API Reference →
Before you begin
- You have a v2 API key for the environment you are calling. See the Quickstart.
- You know the intent (product) the applicant wants, and you have read its current rules. List the intents and their rules with
GET /api/v2/intent; see How your request is checked. - The applicant has consented to their data being processed (POPI).
1. Request offers
The request body uses the same applicant fields as Sending a Lead:
curl -X POST "https://api.engine-v2.uat.fincheck.co.za/api/v2/offer" \
-H "Authorization: Bearer fce_your_key" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Thabo",
"last_name": "Molefe",
"cell_phone_number": "0821234567",
"email": "thabo.molefe@example.com",
"id_number": "8001015009087",
"intent": "Personal Loans",
"popi": true,
"gross_income": 25000,
"net_income": 18000,
"loan_amount_required": 5000
}'
This example includes the income and loan fields that Personal Loans needs; other intents need different fields. Below are the rules every request follows. Send either intent or intent_id.
POST /offer and POST /lead take the same applicant data, so the data rules on this page are the same for both. They differ in two request controls: fast_application belongs to POST /offer (see below), and partner_ids belongs to POST /lead. If you send the other endpoint's control it is checked for type but has no effect.
How your request is checked
Every lead is checked against the rules in the table above, plus the rules of the intent you select: the extra fields it requires, any narrower limits, and any fields only that intent defines. Together they are the validation contract for that request.
Read the contract from the API, every time it matters. Call GET /api/v2/intent and use each intent's schema for its current required fields, limits and intent-specific fields. To read just one intent, call GET /api/v2/intent/{id} with its id. Do not copy an intent's rules into your integration: they can change without a new API version, and a request that was valid yesterday can be rejected today.
{
"id": 2,
"title": "Personal Loans",
"schema": {
"required": ["first_name", "last_name", "cell_phone_number", "id_number", "popi", "gross_income"],
"properties": { "repayment_period": { "maximum": 72 } }
}
}
This is an illustrative excerpt: required lists every field you must send for that intent, and properties carries each field's limits. A required field you leave out is rejected with required; a value outside a limit is rejected with the matching code. See Intent fields for how to read the whole schema.
Every problem found in the body is reported at once. Each field lists the first problem found with it.
Choosing the intent
Send intent (the title) or intent_id (the id), both from GET /intent. You may send both when they name the same intent.
An intent error is the whole answer: field problems are reported once the intent is accepted.
Names, email, mobile and ID numbers
- Names (
first_name,last_name): letters from any alphabet, spaces, hyphens, straight or curly apostrophes and full stops, with at least one letter.Zoë,Ané,D’Angelo,Anne-MarieandC.J.are all accepted. email: an ASCII address with a dotted domain, such asperson@example.comorperson@company.co.za.user@localhostis rejected, and addresses with non-ASCII characters are not supported.cell_phone_number: a South African mobile number (06x, 07x or 081–084), written as0821234567,821234567,27821234567or+27821234567. Spaces, dashes, dots and brackets are allowed, so+27 82 123 4567is accepted. It is stored as0821234567.id_number: a valid South African ID number, 13 digits, for an applicant aged 18–100.
Send every field with its JSON type: numbers for money fields (25000, not "25000"; decimals are rounded to whole rand), whole numbers for counts, true/false for yes-no fields, and strings for text, including id_number and cell_phone_number. A value of the wrong type is rejected with 422. Omit optional fields when no value is available. A null or blank value counts as not sent; for a required field that is reported as required. See Placeholder values if your system uses a placeholder instead. Send the body as Content-Type: application/json; anything else returns 415.
Send what you have: more data gives partners more to match on.
Placeholder values
Leave out fields you don't have, or send JSON null. If your system fills a missing value with a placeholder, v2 also accepts the placeholders below, on the fields listed below, and treats them as not provided. Use only these values:
Placeholders match in any letter case, and spaces before or after are ignored. Other spellings, such as none, na, -, or 0, are validated as real values, so "employed": "none" is rejected.
These fields accept placeholders:
These fields don't treat placeholders as missing:
A placeholder counts as a missing value, so the rules for missing values apply: for example, if net_income is a placeholder and gross_income is sent, net_income defaults to gross_income.
2. Handle validation errors
If a field fails validation, the API returns HTTP 422 with a stable code and message for the first problem found with each field. Every problem is reported at once:
{
"code": "validation_failed",
"message": "Validation failed.",
"errors": {
"gross_income": [{ "code": "required", "message": "is required" }],
"payment_frequency": [{ "code": "not_in_enum", "message": "must be one of: weekly, fortnightly, monthly, other" }]
}
}
The keys in errors are the JSON field names you sent. Fix those values and request offers again. An unknown, inactive or unavailable intent is a separate answer (see Choosing the intent), and Errors lists every code.
In the rare case an intent's settings change while your request is being processed, POST /offer answers intent_schema_validation_failed with a missing_fields list instead. Read the intent again with GET /intent and resend.
3. Show the matches
{
"matches": [
{
"id": 227,
"company_name": "Example Lender",
"company_logo_url": "https://partner.example.com/logo.png",
"partner_type": "url",
"rank": 1,
"probability": 80,
"end_to_end": false
}
],
"id": "k3mN9pQx",
"result": "created"
}
The top-level id is the lead id. result is created for a new lead and duplicate when you already sent this applicant and the existing lead is returned; see Duplicate Submissions. Each item in matches is one partner the applicant can choose. Show them in rank order.
A partner that is offered still makes its own credit decision when the applicant accepts, and probability is an estimate for display, not a decision.
The API Reference lists every field in the response.
All response fields
Quote or apply now
application_method tells Fincheck how the applicant wants to continue:
Leave application_method out, or send get-quote, for an ordinary comparison. Any other value is rejected with not_in_enum.
{
"first_name": "Thabo",
"last_name": "Molefe",
"id_number": "8001015009087",
"cell_phone_number": "0821234567",
"intent": "Personal Loans",
"popi": true,
"gross_income": 25000,
"net_income": 18000,
"loan_amount_required": 5000,
"application_method": "apply-now"
}
End-to-end applications must be enabled for your account. Contact Fincheck before you send apply-now. If your account is not enabled, an apply-now accept continues as a normal referral.
No matches
An empty matches array with HTTP 200 is a valid result: the applicant was evaluated, and no partner currently qualifies.
{
"matches": [],
"id": "k3mN9pQx",
"result": "created"
}
Tell the applicant no offers are available. Do not retry with the same data or invent a partner id.
Next step
When the applicant chooses a partner, accept the offer with the lead id and the match id.