Batch Import
Upload many leads at once, then follow processing by polling or webhook.
Batch import is the Direct integration for many leads at once. You upload a file or a JSON list, FincheckEngine queues it straight away, and each row is processed in the background exactly like a single lead.
See POST /api/v2/lead/batch in the API Reference →
Before you begin
- Each row uses the same fields and rules as Sending a Lead: the rules every lead follows plus those of the batch's intent. Read the intent's current rules from
GET /intentinstead of hardcoding them. - A batch can hold up to 2,000 rows by default. Contact support if you need a different limit.
- You choose one intent for the whole batch.
1. Prepare the CSV
The first row holds the field names. Each following row is one applicant:
first_name,last_name,cell_phone_number,email,id_number,popi,gross_income,net_income,loan_amount_required
Thabo,Molefe,0821234567,thabo.molefe@example.com,8001015009087,true,25000,18000,5000
Lerato,Dlamini,0839876543,lerato.dlamini@example.com,9001014800089,true,32000,24000,8000
Use commas or semicolons as separators. XLSX and XLS files are also accepted, but CSV is the most portable. The sample at the bottom of this page has every supported heading.
To send a row to specific partners, add a partner_ids column. A partner_id column with a value is rejected with "partner_id": "is not supported; use partner_ids"; rename it, or map it with the map field. A blank partner_id cell is ignored.
How cells are read
A CSV or spreadsheet cell is text, so each cell is converted to its field's type before the rules are applied. The rules are the same as for a single lead; only the conversion is specific to files.
- Blank cells mean not sent. Omit optional values you don't have; a blank required cell is reported as
required. - Numbers are written as plain digits, such as
25000or25000.50. Money is rounded to whole rand. A cell that is not a number, such asabc, is rejected for that field. - Lists such as
partner_idsare written in one cell, separated by commas or semicolons:30,44or30;44. Empty entries are skipped. One entry that is not a whole number rejects the cell, and a list holds at most 50 ids. - Yes/no fields such as
popiaccepttrue,false,yes,no,y,n,1or0, in any letter case. - Intent-specific fields are converted by the type the intent defines for them.
2. Upload the file
curl -X POST "https://api.engine-v2.uat.fincheck.co.za/api/v2/lead/batch" \
-H "Authorization: Bearer fce_your_key" \
-H "Accept: application/json" \
-F "leads=@lead-batch.csv;type=text/csv" \
-F "intent=Personal Loans"
Let your HTTP client set the multipart boundary. Do not set Content-Type: multipart/form-data yourself.
If any row fails the rules, nothing is queued and the upload is answered with 422 batch_rows_invalid, listing each bad row once, up to 200 rows:
{
"code": "batch_rows_invalid",
"message": "2 row(s) failed validation",
"row_errors": [
{ "row": 0, "errors": { "id_number": "must be a valid RSA ID number" } },
{ "row": 3, "errors": { "cell_phone_number": "must be a South African mobile number (06x, 07x or 081–084)", "gross_income": "required" } }
]
}
row is the zero-based position of the data row. Unlike a single lead, each field in errors is a plain message, not a {code, message} entry, and lists the first problem found with that field. Correct the rows and upload the batch again.
If the intent is unknown, inactive or temporarily unavailable, the upload is refused with that error (intent_not_found, intent_inactive, or 503 intent_unavailable) before any row is checked.
3. Store the batch id
An accepted upload returns HTTP 202:
{
"batch_id": 1842,
"status": "queued",
"total_rows": 2
}
4. Check the status
Poll with the same key that uploaded the batch:
curl "https://api.engine-v2.uat.fincheck.co.za/api/v2/lead/batch/1842" \
-H "Authorization: Bearer fce_your_key" \
-H "Accept: application/json"
{
"batch_id": 1842,
"status": "completed",
"accepted": 1,
"rejected": 1,
"duplicate": 0,
"error_count": 0,
"processed": 2,
"total_rows": 2,
"webhook_status": "skipped",
"results": [
{ "row": 0, "status": "created", "id": "k3mN9pQx", "lead_state": "dispatched" },
{ "row": 1, "status": "rejected", "errors": { "id_number": "must be a valid RSA ID number" } }
]
}
Poll every 30 seconds or so, and stop when status is completed or failed.
Batch states
Row results
row is the zero-based position of the data row, so the first row after the headings is 0. results lists the first 500 rows. The counters always cover the whole batch.
Completion webhook
Add result_url to receive the final status instead of polling:
-F "result_url=https://your-server.example.com/webhooks/fincheck/batches"
When the batch finishes, FincheckEngine sends a POST with a JSON body identical to the status response above.
- Respond with any
2xxstatus within 10 seconds. Do the processing after you respond. - A failed delivery is retried twice in quick succession. After that,
webhook_statusisfailed. - The request is not signed. Treat it as a signal, and confirm the result by calling
GET /lead/batch/{id}.
webhook_status is pending, sent, failed, or skipped (no result_url). Polling always works, with or without a webhook.
JSON batches
You can send the batch as JSON instead of a file:
{
"intent": "Personal Loans",
"result_url": "https://your-server.example.com/webhooks/fincheck/batches",
"leads": [
{
"first_name": "Thabo",
"last_name": "Molefe",
"cell_phone_number": "0821234567",
"id_number": "8001015009087",
"popi": true
}
]
}
A batch-level partner_ids applies only to rows that do not have their own: a row that omits partner_ids, or leaves it blank, gets the batch's ids. A row whose partner_ids is present but invalid is rejected, never given the batch's ids. Each row takes the same fields as POST /lead, except intent and intent_id, which are set once for the batch.
The upload answers with:
and GET /lead/batch/{id} with:
Troubleshooting
To check on a batch, poll its existing batch_id instead of uploading the file again. Rows of a new upload are checked against your existing leads like any other submission.