Appearance
Bulk Import
This guide walks you through importing a batch of people from a CSV file — for example an export from an HR system or a legacy CRM — and creating a client and a record and, optionally, a case (see beta note below) for each person using the Amiqus API.
By the end of this guide you will have a single PHP script that reads a CSV file, creates clients, cases and records in bulk, and writes a results file recording the outcome of every row.
If you are looking for the equivalent single-person flow triggered by your own application (for example during user registration), see the Standard Amiqus Integration Flow guide.
Prerequisites
- Amiqus API credentials: You will need valid credentials to use the Amiqus API.
- A record template: Each CSV row references a record template by ID. You can list the templates available to your team via the record templates endpoint.
- A unique reference per person: Each CSV row must include a
client_referencefrom your own system. The example script uses it to detect people who were already imported by an earlier run, so re-running the script is safe. - Webhook registration (recommended): Configure a webhook endpoint to receive record update notifications so you can track when each client completes their checks.
- PHP 8.1 or later with the
curlextension, to run the example script.
Cases are a beta feature and may not be available to all clients. Steps that involve cases are marked as optional (beta).
The CSV file
The import script expects a CSV file with a header row. Here is an example, people.csv:
csv
title,first_name,middle_name,last_name,email,client_reference,template_id,case_name,case_reference
mr,Martin,Seamus,McFly,marty@example.com,EMP-0091,12,Onboarding - Martin McFly,CASE-0091
ms,Jennifer,,Parker,jennifer@example.com,EMP-0092,12,Onboarding - Jennifer Parker,CASE-0092
dr,Emmett,,Brown,emmett@example.com,EMP-0093,14,,
template_idvalues12and14above are placeholders. Replace them with real template IDs from your own team before running the script — call the record templates endpoint to list them. An invalidtemplate_idfails with a 422 ("The selected template is invalid") for that row.
| Column | Required | Maps to |
|---|---|---|
title | Optional | Client name.title |
first_name | Required | Client name.first_name |
middle_name | Optional | Client name.middle_name |
last_name | Required | Client name.last_name |
email | Required | Client email |
client_reference | Required | Client reference — an identifier from your own system, and the key the script uses to detect already-imported people |
template_id | Required | Record template — the record template to use for this person |
case_name | Optional (beta) | Case name |
case_reference | Optional (beta) | Case reference |
If neither case_name nor case_reference is present for a row, the case steps are skipped for that person — a client and a record are still created. In the example above, Emmett Brown gets a record but no case.
Overview of the flow
For each row in the CSV, the script looks up the client by reference and creates one only if it doesn't already exist, then always creates a case (optionally) and a record, and — if a case is involved — adds the record to the case. Each row is processed independently: a row that fails is recorded in the results file and the import moves on to the next row.
Making re-runs safe
The API does not deduplicate clients, cases or records for you — creating the same person twice produces two separate clients. This example only guards against the most common failure: a row that already succeeded on a previous run gets a duplicate client if you re-run the script over the same CSV. It does this by looking up the client by client_reference — GET /clients?reference=, which matches exactly (case-insensitive, not fuzzy) — before creating one. This is why client_reference is required in this guide — without it there's no reliable way to find a person that was already imported.
The case and record steps are not deduplicated — if a row's client already existed but the script previously failed partway through (for example after creating the client but before creating their record), re-running that row creates a second case and/or record for the same client. This guide keeps the example simple; a production import should track more than just the client, for example by recording each row's client_id, case_id and record_id against your own internal ID as they're created, and skipping any step whose ID is already recorded before calling the API again.
Step-by-step: the API calls
The steps below show the API payloads for a single row — the first row of the example CSV, assuming its client doesn't exist yet. The complete import script at the end of this guide looks up the client before creating it, as described in Making re-runs safe, and runs these steps for every row.
1. Create a client
List clients filtered by reference first. If none is returned, create a client using the person's name, email and reference.
POST /clients
json
{
"name": {
"title": "mr",
"first_name": "Martin",
"middle_name": "Seamus",
"last_name": "McFly"
},
"email": "marty@example.com",
"reference": "EMP-0091"
}On success, store the client id from the response — 7832 in the examples that follow.
2. (Optional, beta) Create a case
If the row has a case_name or case_reference, create a case for the client.
POST /cases
json
{
"client": 7832,
"name": "Onboarding - Martin McFly",
"reference": "CASE-0091"
}On success, store the case id — 101 in the examples that follow. The case name is required by the API: if a row only provides a case_reference, the example script derives a name from the person's name.
3. Create a record from a template
Create a record for the client using the row's template_id. Using a template keeps every imported record consistent — the checks, documents and settings are defined once, on the template.
POST /records
json
{
"client": 7832,
"template": 12
}On success, store the record id — 983434 in the examples that follow.
Warning: The
notificationfield is not accepted whentemplateis present — the API returns a 422 if you send it. Records created from a template inherit the notification setting configured on the template itself. If your template sends an email notification, a 500-row import sends 500 emails as soon as each record is created. Check and adjust the template's notification setting (via the templates endpoint or your Amiqus dashboard) before running a large import. If you need per-request control over notifications, definestepsmanually instead of using a template — see the Standard Amiqus Integration Flow guide.
4. (Optional, beta) Add the record to the case
If a case was created for the row, add the record to it using the Update Case Items endpoint.
PATCH /cases/101/items
json
[
{
"id": 983434,
"type": "record"
}
]This associates the record with the case for grouped tracking and reporting.
The complete import script
The script below is self-contained — plain PHP with the curl extension, no dependencies. Save it as bulk-import.php, set your access token, and run it with your CSV file:
bash
php bulk-import.php people.csvEach row is processed independently inside a try/catch: a row that fails is recorded with its error and the import continues. On HTTP 429 (rate limited) the script waits and retries the request once, honouring the Retry-After header where present — a production script may want exponential backoff instead. Results are written to results.csv as the import runs.
php
<?php
const API_URL = 'https://id.amiqus.co/api/v2';
const ACCESS_TOKEN = 'your-access-token';
class AmiqusApi
{
public function __construct(private string $apiUrl, private string $accessToken)
{
}
public function findClientByReference(string $reference): ?array
{
$response = $this->request('GET', '/clients?' . http_build_query(['reference' => $reference]));
return $response['data'][0] ?? null;
}
public function createClient(array $name, string $email, string $reference): array
{
return $this->request('POST', '/clients', [
'name' => $name,
'email' => $email,
'reference' => $reference,
]);
}
public function createCase(int $clientId, string $name, string $reference): array
{
$payload = ['client' => $clientId, 'name' => $name];
if ($reference !== '') {
$payload['reference'] = $reference;
}
return $this->request('POST', '/cases', $payload);
}
public function createRecord(int $clientId, int $templateId): array
{
return $this->request('POST', '/records', [
'client' => $clientId,
'template' => $templateId,
]);
}
public function addRecordToCase(int $caseId, int $recordId): array
{
return $this->request('PATCH', '/cases/' . $caseId . '/items', [
['id' => $recordId, 'type' => 'record'],
]);
}
private function request(string $method, string $path, array $payload = [], bool $retried = false): array
{
$ch = curl_init($this->apiUrl . $path);
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
'Authorization: Bearer ' . $this->accessToken,
],
];
if ($payload !== []) {
$options[CURLOPT_POSTFIELDS] = json_encode($payload);
}
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException('Connection error: ' . curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
if ($status === 429 && !$retried) {
$delay = preg_match('/^Retry-After:\s*(\d+)/mi', $headers, $matches) ? (int) $matches[1] : 10;
sleep($delay);
return $this->request($method, $path, $payload, true);
}
if ($status < 200 || $status >= 300) {
throw new RuntimeException('HTTP ' . $status . ': ' . $body);
}
return $body === '' ? [] : (json_decode($body, true) ?? []);
}
}
$csvPath = $argv[1] ?? null;
if ($csvPath === null || !is_readable($csvPath)) {
fwrite(STDERR, "Usage: php bulk-import.php <people.csv>\n");
exit(1);
}
$handle = fopen($csvPath, 'r');
$header = fgetcsv($handle, escape: '');
if ($header === false) {
fwrite(STDERR, "Could not read a header row from {$csvPath}\n");
exit(1);
}
$requiredColumns = ['first_name', 'last_name', 'email', 'client_reference', 'template_id'];
$missingColumns = array_diff($requiredColumns, $header);
if ($missingColumns !== []) {
fwrite(STDERR, 'Missing required CSV columns: ' . implode(', ', $missingColumns) . "\n");
exit(1);
}
$api = new AmiqusApi(API_URL, ACCESS_TOKEN);
$results = fopen('results.csv', 'w');
fputcsv($results, ['row', 'client_id', 'case_id', 'record_id', 'status'], escape: '');
$rowNumber = 0;
$succeeded = 0;
$failed = 0;
while (($values = fgetcsv($handle, escape: '')) !== false) {
if ($values === [null]) {
continue;
}
$rowNumber++;
$clientId = $caseId = $recordId = null;
try {
$row = array_combine($header, $values);
if ($row['client_reference'] === '') {
throw new RuntimeException('client_reference is required to safely re-run the import');
}
$existingClient = $api->findClientByReference($row['client_reference']);
if ($existingClient !== null) {
$clientId = $existingClient['id'];
} else {
$name = array_filter([
'title' => $row['title'] ?? '',
'first_name' => $row['first_name'],
'middle_name' => $row['middle_name'] ?? '',
'last_name' => $row['last_name'],
], fn (string $value): bool => $value !== '');
$client = $api->createClient($name, $row['email'], $row['client_reference']);
$clientId = $client['id'];
}
$caseName = $row['case_name'] ?? '';
$caseReference = $row['case_reference'] ?? '';
if ($caseName !== '' || $caseReference !== '') {
if ($caseName === '') {
$caseName = 'Onboarding - ' . $row['first_name'] . ' ' . $row['last_name'];
}
$case = $api->createCase($clientId, $caseName, $caseReference);
$caseId = $case['id'];
}
$record = $api->createRecord($clientId, (int) $row['template_id']);
$recordId = $record['id'];
if ($caseId !== null) {
$api->addRecordToCase($caseId, $recordId);
}
fputcsv($results, [$rowNumber, $clientId, $caseId, $recordId, 'ok'], escape: '');
$succeeded++;
} catch (Throwable $e) {
fputcsv($results, [$rowNumber, $clientId, $caseId, $recordId, 'error: ' . $e->getMessage()], escape: '');
$failed++;
}
}
fclose($handle);
fclose($results);
echo "Processed {$rowNumber} rows: {$succeeded} succeeded, {$failed} failed.\n";The results file
The script writes results.csv in the directory you run it from, one row per imported person. The IDs let you cross-reference webhook notifications later; the status column is ok or the error that caused the row to fail:
csv
row,client_id,case_id,record_id,status
1,7832,101,983434,ok
2,7833,102,983435,ok
3,7834,,,"error: HTTP 422: {""template"":[""The selected template is invalid""]}"A partially populated row shows how far that person got — for example a row with a client_id but no record_id failed at record creation, so the client already exists in Amiqus and should not be blindly re-imported.
What happens next?
Amiqus emails each client a link to complete their checks. As clients submit their details:
- Amiqus updates each record's status.
- A webhook notification is sent to the endpoint you configured for each record update, allowing you to mark people as verified in your own system. See the webhooks guide for setup and payload details.
You may then want to:
- Confirm the checks passed by retrieving the record or listing its steps.
- Retrieve detailed results using the Check Result Data guide.