Fincheck

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 /intent instead 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:

CSV
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 25000 or 25000.50. Money is rounded to whole rand. A cell that is not a number, such as abc, is rejected for that field.
  • Lists such as partner_ids are written in one cell, separated by commas or semicolons: 30,44 or 30;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 popi accept true, false, yes, no, y, n, 1 or 0, in any letter case.
  • Intent-specific fields are converted by the type the intent defines for them.

2. Upload the file

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

Form fieldRequiredUse
leadsRequiredThe CSV, XLSX, or XLS file.
intent or intent_idRequiredThe intent for every row: its title or id from GET /intent.
mapOptionalA JSON object that renames your headings, for example {"mobile":"cell_phone_number"}.
result_urlOptionalAn absolute HTTP or HTTPS URL that receives the final status. See Completion webhook.
partner_idsOptionalPartner ids for rows that do not have their own, as comma-separated ids or a JSON array string.

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:

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

JSON
{
  "batch_id": 1842,
  "status": "queued",
  "total_rows": 2
}

4. Check the status

Poll with the same key that uploaded the batch:

cURL
curl "https://api.engine-v2.uat.fincheck.co.za/api/v2/lead/batch/1842" \
  -H "Authorization: Bearer fce_your_key" \
  -H "Accept: application/json"
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

StatusWhat it means
queuedAccepted and waiting to start.
processingRows are being processed. processed counts up towards total_rows.
completedEvery row has a result.
failedThe batch could not be processed. Keep the batch_id and contact support.

Row results

StatusWhat it meansWhat to do
pendingNot processed yet.Nothing.
createdA new lead was created. id is the lead id and lead_state is its current state.Store id.
duplicateThe applicant matched a lead you already have, from this or an earlier upload. id is the existing lead's id. Nothing was sent again. See Duplicate Submissions.Store id. Do not resend.
rejectedA field failed validation. errors names each field.Correct the row and send it in a new batch.
errorThe row could not be processed. error describes why.Send the row again later, or contact support.

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:

cURL
-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 2xx status within 10 seconds. Do the processing after you respond.
  • A failed delivery is retried twice in quick succession. After that, webhook_status is failed.
  • 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:

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

FieldRequiredTypeAllowed valuesDescription
leadsRequiredlist of objectsup to 10 000 itemsThe rows to create, 1 to 10 000.
intentOptionalstring—Intent slug when intent_id is omitted.
intent_idOptionalinteger≥ 1Intent id from GET /intent, for every row. Send this or intent.
mapOptionalobject—Renames your field names to API field names (e.g. mobile → cell_phone_number).
partner_idsOptionallist of integers≥ 1 · up to 50 itemsOptional batch-level default partner user IDs. Applied to any lead row that omits partner_ids or leaves it blank; a row with invalid partner_ids is rejected instead. Per-row partner_ids take precedence. Caps at 50. When set, each row is referred only to those partners.
result_urlOptionalstringabsolute URLOptional webhook URL for final status JSON.

The upload answers with:

FieldRequiredTypeAllowed valuesDescription
batch_idRequiredinteger—Batch id; poll GET /lead/batch/{id} for progress.
statusRequiredenumqueuedAlways queued: rows are processed in the background.
total_rowsRequiredinteger—Number of rows accepted for processing.
result_urlOptionalstring—Webhook that receives the final status, as sent.

and GET /lead/batch/{id} with:

FieldRequiredTypeAllowed valuesDescription
batch_idRequiredinteger—Batch id.
processedRequiredinteger—Rows processed so far.
statusRequiredenumqueued, processing, completed, failedTerminal states are completed and failed.
total_rowsRequiredinteger—Rows in the batch.
acceptedOptionalinteger—Rows that created a new lead.
duplicateOptionalinteger—Rows that matched an existing lead.
error_countOptionalinteger—Rows that could not be processed.
rejectedOptionalinteger—Rows that failed validation.
result_urlOptionalstring—Webhook that receives the final status, as sent.
resultsOptionallist of objects—The first 500 rows, in row order. The counters cover the whole batch.
results[].rowRequiredinteger—Zero-based row index in the original batch.
results[].statusRequiredenumpending, created, rejected, duplicate, errorWhat happened to this row. It does not change after processing.
results[].errorOptionalstring—Processing error when status is error.
results[].errorsOptionalobject—Field errors when status is rejected.
results[].idOptionalstring—Lead id when status is created, or the id of your existing lead when duplicate (same rules as POST /lead, including earlier uploads).
results[].lead_stateOptionalstring—The lead's current state when the row has a lead. See the Tracking a Lead guide.
results[].updated_atOptionalstring—When the lead last changed (RFC 3339, UTC), when the row has a lead.
webhook_statusOptionalenumpending, sent, failed, skippedskipped when result_url was not supplied.

Troubleshooting

StatusCauseWhat to do
400The body or file could not be read, or the batch was empty.Check the file format and headings.
401The key is missing or rejected.See Authentication.
403You are reading a batch another affiliate uploaded.Use the key that uploaded it.
404The batch does not exist in this environment.Check the batch_id and environment.
422A batch-level value is wrong (missing or unknown intent, inactive intent, invalid result_url, too many rows), or rows failed the field rules (batch_rows_invalid).Correct the value or rows and upload again.
503The intent is temporarily unavailable (intent_unavailable), or the API is in maintenance.Retry later with backoff.

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.

Sample CSV

Start with the supported headings and two fictional applicants. Keep intent or intent_id on the multipart form. The default limit is 2,000 rows.

Download sample CSV