Skip to content

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_reference from 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 curl extension, 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_id values 12 and 14 above 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 invalid template_id fails with a 422 ("The selected template is invalid") for that row.

ColumnRequiredMaps to
titleOptionalClient name.title
first_nameRequiredClient name.first_name
middle_nameOptionalClient name.middle_name
last_nameRequiredClient name.last_name
emailRequiredClient email
client_referenceRequiredClient reference — an identifier from your own system, and the key the script uses to detect already-imported people
template_idRequiredRecord template — the record template to use for this person
case_nameOptional (beta)Case name
case_referenceOptional (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_referenceGET /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 id101 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 id983434 in the examples that follow.

Warning: The notification field is not accepted when template is 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, define steps manually 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.csv

Each 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:

  1. Confirm the checks passed by retrieving the record or listing its steps.
  2. Retrieve detailed results using the Check Result Data guide.