Fincheck

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

FieldRequiredTypeAllowed valuesDescription
cell_phone_numberRequiredstringSA mobile: 06x, 07x or 081–084South African mobile number (06x, 07x or 081–084) as 0821234567, 821234567, 27821234567 or +27821234567. Stored as 0821234567.
first_nameRequiredstringup to 255 characters · letters, spaces, hyphens, apostrophes, full stopsApplicant's first name: letters (including accented ones, e.g. Zoë), spaces, hyphens, apostrophes (' or ’) and full stops, with at least one letter.
id_numberRequiredstringSA ID number · applicant aged 18–10013-digit South African ID number. Age and gender are derived from it; the applicant must be 18–100.
last_nameRequiredstringup to 255 characters · letters, spaces, hyphens, apostrophes, full stopsApplicant's surname. Same rules as first_name.
popiRequiredbooleanmust be trueThe applicant consented to their data being processed. Must be true.
application_methodOptionalenumget-quote, apply-nowHow the applicant wants to continue: get-quote compares offers and continues with the chosen partner; apply-now applies end to end inside your journey. Omitted means get-quote. An end-to-end accept needs apply-now.
bank_nameOptionalstringup to 255 charactersApplicant's bank.
cityOptionalstringup to 255 charactersCity or town.
company_nameOptionalstringup to 255 charactersApplicant's employer.
credit_score_intOptionalinteger0 – 999Credit score from your own bureau check.
credit_score_labelOptionalstringup to 64 charactersCredit score band from your own bureau check.
debt_amountOptionaldecimal0 – 99 999 999,99Total outstanding debt in rand.
debt_reviewOptionalbooleantrue / falseThe applicant says they are under debt review.
educationOptionalstringup to 64 charactersHighest level of education.
emailOptionalstringup to 255 characters · email address with a dotted domainApplicant's email address, as name@example.com: ASCII only, and the domain must contain a dot.
employedOptionalbooleantrue / falseWhether the applicant is currently employed. When omitted it is derived from employment_type; when both are sent they must agree.
employment_periodOptionalstringup to 255 charactersHow long the applicant has been employed.
employment_typeOptionalenumfull_time, part_time, self_employed, contract, commission, pensioner, student, homemaker, unemployed, otherApplicant's employment status. Sets employed when employed is not sent.
expensesOptionaldecimal0 – 10 000 000Monthly expenses in rand. Decimals are rounded to whole rand.
fast_applicationOptionalbooleantrue / falsePOST /offer only: true skips the requirements the intent added (listed in x-intent-required on GET /intent/{id}) because the applicant is already pre-qualified. The base required fields still apply.
gross_incomeOptionaldecimal0 – 10 000 000Monthly income before deductions, in rand. Decimals are rounded to whole rand. If only one of gross_income and net_income is sent, the other is set to the same value.
income_sourceOptionalenumsalary, wage, commission, self-employed, sassa, unemployed, otherMain source of income.
intentOptionalstringup to 255 charactersIntent title from GET /intent, matched case-insensitively. Send this or intent_id.
intent_idOptionalinteger1 – 2 147 483 647Intent id from GET /intent. Send this or intent.
languageOptionalstringup to 10 charactersApplicant's preferred language.
loan_amount_requiredOptionaldecimal1 – 10 000 000Amount the applicant wants to borrow, in rand. Decimals are rounded to whole rand.
net_incomeOptionaldecimal0 – 10 000 000 · not more than gross_incomeMonthly take-home pay in rand; may not exceed gross_income. Decimals are rounded to whole rand.
open_loansOptionalinteger0 – 100Number of loans the applicant currently has.
paydayOptionalinteger1 – 31Day of the month the applicant is paid.
payment_frequencyOptionalenumweekly, fortnightly, monthly, otherHow often the applicant is paid.
postal_codeOptionalstringup to 16 charactersPostal code.
provinceOptionalstringup to 64 charactersProvince.
referee_idOptionalstringup to 32 charactersAttributes the lead to the affiliate whose referee_id matches this value.
repayment_periodOptionalinteger1 – 120Requested loan term in months.
street_addressOptionalstring—Street address.
suburbOptionalstringup to 255 charactersSuburb.
titleOptionalstringup to 255 charactersSalutation, e.g. Mr, Mrs, Miss, Dr, Prof.

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.

JSON
{
  "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.

SituationResponse
Neither is sent422 validation_failed, with intent listed as required: "send one of: intent, intent_id".
Both are sent and name different intents422 validation_failed, with intent listed as invalid_value: "must match intent_id".
No such intent422 intent_not_found.
The intent is not accepting leads422 intent_inactive.
The intent is temporarily unavailable503 intent_unavailable. Retry later with backoff.

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-Marie and C.J. are all accepted.
  • email: an ASCII address with a dotted domain, such as person@example.com or person@company.co.za. user@localhost is rejected, and addresses with non-ASCII characters are not supported.
  • cell_phone_number: a South African mobile number (06x, 07x or 081–084), written as 0821234567, 821234567, 27821234567 or +27821234567. Spaces, dashes, dots and brackets are allowed, so +27 82 123 4567 is accepted. It is stored as 0821234567.
  • 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:

PlaceholderExample
"" (empty string)"city": ""
null written as text"payday": "NULL"
unknown"employed": "Unknown"
n/a"bank_name": "N/A"
not specified"education": "Not specified"
undefined"province": "undefined"

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:

GroupFields
Contactemail
Personaltitle, language, education
Employment and incomeemployed, income_source, employment_type, employment_period, company_name, payday, gross_income, net_income, expenses, payment_frequency
Banking and creditbank_name, loan_amount_required, repayment_period, debt_amount, open_loans, debt_review, credit_score_label, credit_score_int
Addressstreet_address, suburb, city, postal_code, province

These fields don't treat placeholders as missing:

FieldsWhat to do
first_name, last_name, cell_phone_number, id_number, popi, intent_id / intentRequired. A placeholder here is checked as a real value: it is rejected, or for names stored as the applicant's name. Send the real value.
referee_id, partner_ids, fast_applicationLeave the field out when you don't have a value.
Any other field, including intent-specific fieldsStored exactly as sent. Leave the field out when you don't have a value.

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:

JSON
{
  "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

JSON
{
  "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.

FieldHow to use it
idThe partner's id. Send it as partner_id when accepting.
company_name, company_logo_urlWhat to show the applicant. Use company_mini_logo_url or company_wide_logo_url for other layouts.
rankFincheck's ordering, starting at 1.
probabilityA likelihood score for display, usually between 0 and 100. 0 means no estimate is available.
live_quotesThe partner's live quote for this applicant, when it provides one. Its shape is the partner's own. Show amounts as estimates; the partner confirms when the applicant accepts. Pass the chosen quote back when accepting. A match can have no live_quotes even when live_scoring_api is true: the partner is offered for this applicant without a price.
probability_conditionsThe partner's probability rules, as a JSON array. For information only.
end_to_endtrue when the partner supports an end-to-end application. See Quote or apply now.

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

FieldRequiredTypeAllowed valuesDescription
idRequiredstring—Lead hashid.
matchesRequiredlist of objects—Partners the applicant qualifies for, best first (by rank).
matches[].company_logo_urlRequiredstring—Partner logo (square). Empty string when none is uploaded.
matches[].company_mini_logo_urlRequiredstring—Small partner logo for compact layouts. Empty string when none is uploaded.
matches[].company_nameRequiredstring—Partner's display name.
matches[].company_website_urlRequiredstring—Partner's website.
matches[].company_wide_logo_urlRequiredstring—Wide partner logo for banners. Empty string when none is uploaded.
matches[].end_to_endRequiredbooleantrue / falseTrue when the partner supports an end-to-end application. To use it, send application_method apply-now on POST /offer and type apply-now on POST /accept; your account must be enabled for end-to-end.
matches[].idRequiredinteger—Partner user id. Pass this as partner_id on POST /accept.
matches[].live_scoring_apiRequiredbooleantrue / falseTrue when the partner can price or pre-approve applicants in real time. A match can have live_scoring_api true and no live_quotes: the partner is offered for this lead without a price.
matches[].partner_slugRequiredstring—Stable partner identifier.
matches[].partner_typeRequiredstring—How accepting this partner works: url (the applicant is redirected), api (the lead is sent to the partner's API) or dashboard (the partner collects it in their portal).
matches[].probabilityRequiredinteger—0 when no probability condition matches.
matches[].probability_conditionsRequiredlist—The partner's probability rules. JSON array, not a JSON-encoded string.
matches[].rankRequiredinteger—Integer. Not a string.
matches[].live_quotesOptionalany—Present when the partner returned quotes for this lead. JSON value, not a string; the shape is the partner's own.
resultRequiredenumcreated, duplicatecreated for a new lead; duplicate when your own earlier lead for this applicant is returned instead. Offer responses include result and leave out status. POST /lead reports that same duplicate with status "Lead already received". See the Duplicate Submissions guide.

Quote or apply now

application_method tells Fincheck how the applicant wants to continue:

ValueUse it whenWhat happens on accept
get-quote (default)The applicant is comparing offers.The applicant continues with the chosen partner: a redirect, or the partner receives the lead.
apply-nowThe applicant applies inside your journey.With "type": "apply-now" on POST /accept, a partner with end_to_end: true receives a complete application and returns a link for the applicant to finish.

Leave application_method out, or send get-quote, for an ordinary comparison. Any other value is rejected with not_in_enum.

JSON
{
  "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.

JSON
{
  "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.