Fincheck

Sending a Lead

Submit an applicant that FincheckEngine matches and sends to partners for you.

POST /api/v2/lead is the Direct integration. You send the applicant once, and FincheckEngine validates them, matches them to eligible partners, and sends the lead to those partners in the background.

See POST /api/v2/lead 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. Send the applicant

cURL
curl -X POST "https://api.engine-v2.uat.fincheck.co.za/api/v2/lead" \
  -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 lead 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.
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.
partner_idsOptionallist of integers1 – 2 147 483 647 · up to 50 itemsPOST /lead only: partner user ids to refer the lead to after it is created. At most 50.
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.

The controls on this endpoint are intent / intent_id, referee_id and partner_ids (see Send to specific partners). fast_application belongs to POST /offer: if you send it here 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. Store the lead id

A successful request returns HTTP 200:

JSON
{
  "id": "k3mN9pQx",
  "status": "Lead successfully added",
  "result": "created"
}

result is duplicate when you already sent this applicant and the existing lead is returned; see Duplicate submissions.

FieldRequiredTypeAllowed valuesDescription
idRequiredstring—Lead id. For result duplicate, the id of your existing lead.
resultRequiredenumcreated, duplicatecreated for a new lead; duplicate when your own earlier lead for this applicant is returned instead (status "Lead already received"). See the Duplicate Submissions guide.
statusRequiredstring—Lead successfully added, or Lead already received when result is duplicate.
responseOptionalobject—Present on POST /lead only when live feedback is enabled for the affiliate. Omitted on the partner_ids path.
response.stateRequiredenumaccepted, declinedaccepted when a partner took the lead, declined when none did.
response.partnerOptionalstring—Company name when state is accepted, otherwise null.

When you send partner_ids, the response also lists which partners received the lead:

FieldRequiredTypeAllowed valuesDescription
errorsRequiredlist of objects—Partners the lead was not sent to, each with the reason.
errors[].messageRequiredstring—Why the lead was not referred to this partner.
errors[].partner_idRequiredinteger—The partner_ids entry that was not referred.
referred_partner_idsRequiredlist of integers—Partners the lead was sent to.

Store id with your own applicant reference. You need it to track the lead and for any support query.

3. Handle validation errors

If a field fails validation, the API returns HTTP 422 and names each field you need to correct, with a stable code and a message:

JSON
{
  "code": "validation_failed",
  "message": "Validation failed.",
  "errors": {
    "first_name": [{ "code": "invalid_format", "message": "must contain at least one letter, and only letters, spaces, hyphens, apostrophes or full stops" }],
    "cell_phone_number": [{ "code": "invalid_format", "message": "must be a South African mobile number (06x, 07x or 081–084)" }],
    "gross_income": [{ "code": "invalid_type", "message": "must be a number" }],
    "popi": [{ "code": "invalid_value", "message": "Client must give consent." }]
  }
}

The keys in errors are the JSON field names you sent, and each lists the first problem found with that field. Branch on code; messages may be reworded. Correct those values and send the lead again. See Errors for every code, and for business-rule failures such as an inactive intent.

Send to specific partners

If Fincheck has agreed a targeted referral with you, send partner_ids to refer the lead only to those partners instead of the partners FincheckEngine would match:

JSON
{
  "first_name": "Thabo",
  "last_name": "Molefe",
  "cell_phone_number": "0821234567",
  "id_number": "8001015009087",
  "intent": "Personal Loans",
  "popi": true,
  "partner_ids": [227, 311]
}

The request waits for each referral and reports the outcome per partner:

JSON
{
  "id": "k3mN9pQx",
  "status": "Lead successfully added",
  "referred_partner_ids": [227],
  "errors": [{ "partner_id": 311, "message": "Partner not eligible" }]
}

You can send up to 50 partner ids. The singular partner_id is not supported: sending it with a value returns 422, with partner_id listed as unsupported_field ("is not supported; use partner_ids"). Use partner_ids.

Duplicate submissions

If you send the same applicant again, the response's result says what happened:

  • duplicate: you already have this lead, and its id is returned. Nothing is sent again.
  • created: a new lead. When the earlier lead was already sent to partners, the new one only goes to partners that have not received the applicant recently.

A partner never receives the same applicant twice from your partner_ids within its repeat window; such a partner is listed in errors as already sent to this partner. See Duplicate Submissions for the full rules.