Upgrade to v2
Move an existing v1 integration to API v2.
This guide is for integrations that call /api/v1 today. New integrations should start with the Quickstart.
What's new in v2
- Bearer API keys. Every request authenticates with
Authorization: Bearer fce_….api_tokenin the body or query string no longer authenticates. - One authentication error. A rejected key returns HTTP
401with a JSONerroron every endpoint, instead of a404array on offer and accept. - Consistent JSON errors. Every error is JSON, with the same shapes on every endpoint, whatever
Acceptheader you send. - Optional email.
emailis no longer required. - Stricter phone numbers.
cell_phone_numbermust be a South African mobile number, in any common written form, and is stored in one format. - Validated targeted referrals. Leads sent with
partner_idsare now validated like any other lead, capped at 50 partners, and the response reports each referral. - Duplicate handling. Sending the same applicant again returns the lead you already have, or creates a new one that only goes to partners the applicant has not been sent to. Responses say which with
result, and you only ever see your own leads. See Duplicate Submissions. - Typed offer fields.
rankandprobabilityare integers,probability_conditionsis an array, every match has the same flat keys, and each value appears once. - Typed requests.
POST /leadandPOST /offertake each field in its JSON type and name any field sent with the wrong one. - Private lead lookups.
GET /lead/{hashid}and the partner link only return your own leads. - Stable error codes. Every error body has a string
code(for examplelead_already_accepted) and amessage; validation errors list acodeandmessagefor each problem on each field. See Errors. - Safer accepts.
partner_idis validated, and a lead can only be accepted in a state that allows it.
Deprecations
api_tokenauthentication is removed.- The singular
partner_idonPOST /leadandPOST /offeris rejected. Usepartner_ids. company_logo_path,live_score, anduserableare no longer returned in offers. Usecompany_logo_url,live_quotes, and the top-levelpartner_typeandprobability_conditions.live_scoreonPOST /acceptis rejected. Uselive_quotes.- The integer
code(a copy of the HTTP status) is no longer returned. Error bodies carry a stringcodeinstead; success bodies have none. - The live-score marker
-2is gone. Partners whose live score is unavailable are left out ofmatches.
Upgrade your integration
1. Request v2 credentials
Email support@finch-technologies.com for a UAT key and, once you are ready, a Production key. See Environments.
2. Change the complete base URL
Do not keep the v1 host and change only the path. Use the complete base URL for the environment:
The operation names stay the same; see the endpoint mapping.
3. Switch to Bearer authentication
Before (v1):
POST /api/v1/lead?api_token=your_v1_token
Content-Type: application/json
After (v2):
POST /api/v2/lead
Authorization: Bearer fce_your_key
Content-Type: application/json
Use referee_id in the body when a lead must be attributed to another affiliate.
4. Update lead payloads
POST /lead, POST /offer, and each row of POST /lead/batch share these fields. Rows marked Changed or Removed need your attention.
On POST /lead and POST /offer, send the body as Content-Type: application/json and each field in its JSON type: numbers for money (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. v1 accepted numeric strings such as "25000" and words such as "yes"; v2 rejects them with 422 and names the field, for example "gross_income": [{"code": "invalid_type", "message": "must be a number"}]. A body with another Content-Type returns 415.
The other optional fields are title, language, education, employed, payday, bank_name, repayment_period, employment_period, company_name, debt_amount, open_loans, payment_frequency, debt_review, street_address, suburb, city, postal_code, and province.
Intent-specific fields
In v1, POST /lead and POST /offer both enforced the extra fields an intent requires, such as income and loan amount for Personal Loans. v2 does too, on every batch row as well, and reports each missing field in the usual errors list. An intent can also narrow a field's range, for example a shorter maximum repayment_period. Each intent's rules are in its schema from GET /intent; see Intent Fields.
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.
The lead response
The success body drops code and adds result:
{
"id": "k3mN9pQx",
"status": "Lead successfully added",
"result": "created"
}
- Duplicates. v1 created a new lead on every call. v2 checks the applicant against your own leads: while your earlier lead has not been sent to a partner, you get it back with
"result": "duplicate"and statusLead already received; once it has been sent, you get a new lead ("result": "created") that skips partners the applicant already reached. Sending the samepartner_idsagain returns the earlier lead and sends nothing. See Duplicate Submissions. - Live feedback. If your account has live feedback enabled, v1 put the outcome under a key named
"0". v2 puts it underresponse, and omits it when you sendpartner_ids:
{
"id": "k3mN9pQx",
"status": "Lead successfully added",
"response": { "state": "accepted", "partner": "Example Lender" }
}
5. Update offer handling
The response has matches, the lead id, and the new result (created or duplicate), with no code. Each match changes.
Before (v1):
{
"matches": [
{
"id": 227,
"company_name": "Example Lender",
"company_logo_path": "images/partners/227/logo/logo.png",
"company_logo_url": "https://example.com/storage/images/partners/227/logo/logo.png",
"company_website_url": null,
"rank": "1",
"company_mini_logo_url": null,
"company_wide_logo_url": null,
"probability": 80,
"live_score": { "url": "https://partner.example.com/apply/k3mN9pQx" },
"end_to_end": false,
"userable": {
"partner_type": "api",
"probability_conditions": "[{\"field\":\"gross_income\",\"value\":7000,\"operator\":\"greater_than\",\"results\":80}]"
}
},
{
"id": 311,
"company_name": "Second Lender",
"company_logo_path": null,
"company_logo_url": null,
"company_website_url": "https://second.example.com",
"rank": "2",
"company_mini_logo_url": null,
"company_wide_logo_url": null,
"probability": null,
"live_score": -2,
"end_to_end": false,
"userable": { "partner_type": "api", "probability_conditions": null }
}
],
"id": "k3mN9pQx"
}
After (v2):
{
"matches": [
{
"id": 227,
"company_name": "Example Lender",
"company_website_url": "",
"company_logo_url": "https://partner.example.com/logo.png",
"company_mini_logo_url": "",
"company_wide_logo_url": "",
"partner_type": "api",
"probability_conditions": [
{ "field": "gross_income", "value": 7000, "operator": "greater_than", "results": 80 }
],
"partner_slug": "example-lender",
"rank": 1,
"probability": 80,
"live_scoring_api": true,
"live_quotes": { "url": "https://partner.example.com/apply/k3mN9pQx" },
"end_to_end": false
}
],
"id": "k3mN9pQx",
"result": "created"
}
The second lender is not in matches because its live score was unavailable.
GET /intent returns only active intents. Do not reuse an intent id from v1 or from another environment.
6. Update accept handling
A v2 accept request:
{
"hashid": "k3mN9pQx",
"partner_id": 227,
"live_quotes": { "url": "https://partner.example.com/apply/k3mN9pQx" }
}
v2 also checks the lead's state. You can only accept a lead that POST /offer matched, or one whose previous accept was declined or failed. v1 had no such check.
Accept responses
See Accepting an Offer for how to handle each response.
7. Update error handling
In v1, error bodies differed by endpoint, and validation errors came back as JSON only when you sent Accept: application/json; otherwise Laravel redirected. In v2 every error is JSON with a fixed shape.
Authentication
Before (v1, offer and accept):
["Cannot be authorised or authenticated."]
After (v2, every endpoint):
{ "error": "unauthorized" }
Stop treating a 404 array as an authentication failure. Treat any 401 as a key problem.
Validation
Before (v1, POST /lead):
{
"message": "The required fields for the requested Intent are missing",
"errors": {
"first_name": ["The first name field is required."],
"id_number": ["The ID Number field is invalid"]
}
}
After (v2, every endpoint):
{
"code": "validation_failed",
"message": "Validation failed.",
"errors": {
"first_name": [{ "code": "required", "message": "is required" }],
"id_number": [{ "code": "invalid_format", "message": "must be a valid RSA ID number" }],
"gross_income": [{ "code": "invalid_type", "message": "must be a number" }]
}
}
The keys in errors are still your JSON field names; each holds one entry per problem. Branch on code, which never changes; the message is for people and may be reworded. The messages compare with v1 like this:
Missing intent fields
v1 listed each missing intent field as a validation error, on /lead and /offer. v2 does the same on /lead, /offer and every batch row: each missing field appears in errors with the code required. In the rare case an intent's settings change while a request is processed, /offer instead answers intent_schema_validation_failed with a missing_fields list.
Business rules
Other errors
See Errors for retry guidance.
8. Verify in UAT
With your UAT key and origin:
- Call
GET /api/v2/intentand confirm HTTP200. - Send a fictional applicant through the flow you use:
POST /lead, orPOST /offerthenPOST /accept. - Send one invalid applicant and confirm your client reads the
422field errors. - Compare the v1 and v2 results for the same fictional applicant.
Keep Production traffic on v1 while you test.
9. Switch Production
When Fincheck approves your UAT run, change your Production client in one step:
- Replace the v1 token with your Production
fce_key. - Set the base URL to
https://api.engine-v2.fincheck.co.za/api/v2. - Call
GET https://api.engine-v2.fincheck.co.za/api/v2/intentbefore sending a live applicant. - Record the lead
idfrom your first live requests.
10. Stop calling v1
Once Production traffic succeeds on v2, remove every /api/v1 call and api_token from your integration.
Endpoint mapping
POST /lead/batch also changed shape: v1 took a semicolon-separated CSV only. v2 accepts CSV, XLSX, XLS, or JSON, validates each row, and reports per-row results. Rows follow the same duplicate rules as POST /lead, and the partner column must be partner_ids (a partner_id column with a value is rejected). See Batch Import.
See the Changelog for the v1 retirement date once it is announced.