> For the complete documentation index, see [llms.txt](https://docs.delos.financial/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.delos.financial/batchservice/bulk-client-import.md).

# Bulk Client Import

Bulk Client Import lets you submit many customers for KYC/KYB verification in a single **batch** instead of creating and onboarding each one through separate API calls. You describe all customers in one `manifest.json` file, upload it together with their documents to a dedicated storage prefix, and start processing. The platform validates the data, creates the customers, submits them for compliance review and reports the outcome for every row.

A batch can contain **individuals**, **companies** (with their associated parties: UBOs, directors and authorized signatories), or both.

All Bulk Client Import methods live under `/v2/compliance/batch` and belong to the **BatchService** group of the Partner API.

***

**When to use it**

* Migrating an existing customer base to Delos.
* Onboarding customers in bulk from your CRM or back office.
* Any case where calling Add Customer + application methods one by one would be slow or hard to track.

For onboarding a single customer, keep using [Add Customer](https://docs.delos.financial/customer/add-customer) and [Get Compliance Link](https://docs.delos.financial/compliance/get-compliance-link).

***

**Key concepts**

| Term                | Meaning                                                                                                                                                                                                            |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Batch**           | A container for one import. It has a lifecycle status, row counters and its own storage prefix.                                                                                                                    |
| **Schema snapshot** | The verification requirements (fields, documents, questionnaire, associated-party roles) frozen when the batch is created. The batch is always validated against this snapshot, even if requirements change later. |
| **Manifest**        | `manifest.json` in the root of the batch prefix. One item (row) per customer.                                                                                                                                      |
| **Row**             | One customer in the manifest, identified by your `row_key`. Each row has its own status and result.                                                                                                                |
| **Prefix**          | A private folder in Delos storage (S3-compatible) where you upload the manifest and documents using temporary credentials.                                                                                         |

***

**Batch lifecycle**

| Status             | Meaning                                                           | Uploads allowed |
| ------------------ | ----------------------------------------------------------------- | --------------- |
| `OPEN`             | Created and waiting for files and a manifest.                     | Yes             |
| `CLOSED`           | Committed. The manifest is accepted and rows are being processed. | No              |
| `CHANGE_REQUESTED` | Reopened by you for corrections after a commit.                   | Yes             |
| `CANCELLED`        | Closed permanently. Final state.                                  | No              |

**Transitions**

| Action          | From                       | To                 | Other states                                     |
| --------------- | -------------------------- | ------------------ | ------------------------------------------------ |
| Commit          | `OPEN`, `CHANGE_REQUESTED` | `CLOSED`           | `409`                                            |
| Request changes | `CLOSED`                   | `CHANGE_REQUESTED` | No-op from `CHANGE_REQUESTED`, `409` from `OPEN` |
| Cancel          | `OPEN`, `CHANGE_REQUESTED` | `CANCELLED`        | No-op from `CANCELLED`, `409` from `CLOSED`      |

You can have at most **3 batches that are not closed** at the same time. Cancel batches you no longer need to free a slot.

***

**Integration flow**

**Base URL (Sandbox):** `https://partner.sandbox.delos.financial`

All requests require the `Authorization: Bearer <JWT>` header. The partner is identified from the token; you never pass a partner ID.

**Step 1. Create a batch**

Select which customer types this batch will contain.

```bash
curl --request POST \
  --url <https://partner.sandbox.delos.financial/v2/compliance/batch> \
  --header 'Authorization: Bearer <JWT>' \
  --header 'Content-Type: application/json' \
  --data '{ "types": ["individual", "company"] }'
```

The response (`201`) is a `Batch` object. Save `batch_id` and `schema_version`. At this moment the platform freezes the schema snapshot for the selected types.

**Step 2. Get the schema and the example manifest**

```bash
GET /v2/compliance/batch/{batch_id}/schema
GET /v2/compliance/batch/{batch_id}/example-manifest
```

The schema tells you exactly what to provide for each customer:

| Schema part          | Describes                                                              | Filled in the manifest as |
| -------------------- | ---------------------------------------------------------------------- | ------------------------- |
| `fields`             | Profile fields: required flag, value type, format                      | `fields`                  |
| `documents`          | Allowed document types and document groups                             | `documents`               |
| `questionnaire`      | Questionnaire sections and questions: id, type, options, required flag | `questionnaire`           |
| `associated_parties` | Companies only: required roles (UBO, director, etc.)                   | `associated_parties`      |

Use `individual_schema` for individual rows and `company_schema` for company rows. Each role in `company_schema.associated_parties` references a level; the fields and documents for that party are in `associated_party_levels` under that reference.

**Fastest start:** download the example manifest. It is generated from your batch's frozen schema and contains one row per selected type, every field with a `"<required>"` / `"<optional>"` placeholder, sample documents, questionnaire answers in the correct shape, and one party for each required company role. Replace the placeholders, remove optional parts you do not need, and save it as `manifest.json`. The same file is also placed in your prefix as `example_manifest.json`; the platform never reads it as data.

**Step 3. Get storage credentials and upload documents**

```bash
GET /v2/compliance/batch/{batch_id}/credentials
```

The response contains `bucket`, `prefix`, `region` and temporary `credentials` (`access_key_id`, `secret_access_key`, `session_token`, `expiration`). Credentials only work inside your batch prefix and are valid for about **12 hours**. Request new ones with the same call when they expire. Credentials are issued only while the batch is `OPEN` or `CHANGE_REQUESTED`.

Upload all customer documents into the prefix. The folder structure is up to you; file paths in the manifest must match where you put the files.

```bash
export AWS_ACCESS_KEY_ID=<access_key_id>
export AWS_SECRET_ACCESS_KEY=<secret_access_key>
export AWS_SESSION_TOKEN=<session_token>

aws s3 cp ./documents s3://<bucket>/<prefix>/ --recursive --region <region>
```

**Tip:** add `?format=credential_process` to receive credentials in the AWS `credential_process` format. The AWS CLI/SDK will then refresh them automatically:

```
# ~/.aws/config
[profile delos-batch]
credential_process = sh -c 'curl -s -H "Authorization: Bearer $DELOS_TOKEN" "<https://partner.sandbox.delos.financial/v2/compliance/batch/><batch_id>/credentials?format=credential_process"'
```

**File requirements**

* Formats: **PDF, JPEG, PNG**. The file content is checked, not the extension.
* Maximum size: **50 MB** per file.
* Corrupted, password-protected or truncated files are rejected.

**Step 4. Compose and upload `manifest.json`**

Upload one `manifest.json` to the **root** of the prefix.

```bash
aws s3 cp ./manifest.json s3://<bucket>/<prefix>/manifest.json --region <region>
```

**Step 5. Commit the batch**

```bash
POST /v2/compliance/batch/{batch_id}/commit
```

Make sure every file and the manifest are fully uploaded first. A successful commit returns `202`: the batch becomes `CLOSED` and processing starts. Row validation runs asynchronously after the commit.

Do not overwrite `manifest.json` after a commit. The platform verifies the content of the accepted manifest and stops processing if it has been replaced.<br>

**Step 6. Track results**

| Call                                                  | Use it to                                                                                 |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `GET /v2/compliance/batch/{batch_id}`                 | See row counters and the `processed` / `done` flags.                                      |
| `GET /v2/compliance/batch/{batch_id}/items`           | List rows, optionally filtered by status.                                                 |
| `GET /v2/compliance/batch/{batch_id}/items/{row_key}` | Get one row with full review details and rejection reasons.                               |
| `GET /v2/compliance/batch/{batch_id}/status`          | Get a summary of problems with readable explanations, plus the list of created customers. |

* `processed: true` means every row has reached `submitted`, `invalid` or `failed`: onboarding is complete on your side.
* `done: true` means every row has a final result.

A `report.json` file with the outcome of every row also appears in your prefix. You can download it with credentials obtained before the commit, as long as they have not expired.

***

**Manifest format**

The manifest is a JSON object with an `items` array, one item per customer. You can optionally include `schema_version`; if it is omitted or empty, the batch's frozen snapshot is used. A `schema_version` from another batch is rejected.

**Row fields**

| Field                  | Type     | Required              | Description                                                                                                                                       |
| ---------------------- | -------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `row_key`              | `string` | Yes                   | Your unique identifier of the row. Must be unique within the manifest. You use it to find the row's result.                                       |
| `external_customer_id` | `string` | No                    | Your stable customer identifier. Prevents the same customer from being created twice, in this or any other batch. Cannot be changed after commit. |
| `type`                 | `string` | Yes                   | `individual` or `company`. Must be one of the types selected when the batch was created.                                                          |
| `fields`               | `object` | Yes                   | Profile field values, keyed by field id from the schema.                                                                                          |
| `documents`            | `array`  | Yes                   | Documents: `type`, optional `sub_type`, `file` (path relative to the prefix).                                                                     |
| `questionnaire`        | `object` | If the schema has one | Answers: `sections` → section id → `items` → question id → answer.                                                                                |
| `associated_parties`   | `array`  | Companies only        | Company participants: `party_key`, roles in `types`, their own `fields` and `documents`.                                                          |

**Individual row example**

```json
{
  "items": [
    {
      "row_key": "client-001",
      "external_customer_id": "crm-48213",
      "type": "individual",
      "fields": {
        "firstName": "Anna",
        "lastName": "Johnson",
        "email": "anna@example.com",
        "phone": "+35799000000",
        "tin": "123456789",
        "taxResidenceCountry": "CY"
      },
      "documents": [
        { "type": "ID_CARD", "sub_type": "FRONT_SIDE", "file": "client-001/id-front.jpg" },
        { "type": "ID_CARD", "sub_type": "BACK_SIDE", "file": "client-001/id-back.jpg" },
        { "type": "UTILITY_BILL", "file": "client-001/address.pdf" }
      ],
      "questionnaire": {
        "sections": {
          "sofandsow": {
            "items": {
              "sof": { "value": "salary" },
              "sow": { "value": "Savings from salary" }
            }
          },
          "regulatory_info": {
            "items": {
              "highriskindustries": { "values": ["none"] }
            }
          },
          "bankstatement": {
            "items": {
              "bankstatement": { "files": ["client-001/bank.pdf"] }
            }
          }
        }
      }
    }
  ]
}
```

Field ids, document types, questionnaire section and question ids above are examples. Always take them from **your batch's** schema or example manifest.<br>

**Validation rules**

* **Fields.** Every field with `required: true` must have a non-empty value of the correct type. If the schema defines a `format`, the value must match it:
  * date: `YYYY-MM-DD`
  * country: ISO 3166-1 alpha-2 code (e.g. `CY`)
  * email: a valid address with `@` and a domain
  * phone: E.164 (`+` and up to 15 digits, no spaces or brackets)
* **Markup.** Values containing `<` or `>` are rejected in profile fields, free-text questionnaire answers and `external_customer_id`.
* **Documents.** The type must be allowed by the schema, and `file` must point to an uploaded file.
  * Two-sided documents (ID card, residence permit, driver's licence) are sent as **two entries of the same type**with `sub_type` `FRONT_SIDE` and `BACK_SIDE`. If you have both sides, send both. A back side without a front side is not accepted.
  * A passport has no sides; send one entry without `sub_type`.
  * For companies, the document type is always `COMPANY_DOC`; the specific document kind goes in `sub_type`. Every required company document group must be covered by at least one file.
* **Questionnaire.** The answer shape depends on the question type:

  * regular question: `value`
  * multiple-choice question: `values` (each must be one of the question's `options`)
  * file question: `files`, a list of paths. Never put a file path in `value`.

  All required visible questions must be answered.
* **Associated parties (companies).** Each party has its own `party_key`, roles in `types` (`UBO`, `DIRECTOR`, `AUTHORIZED_SIGNATORY`), `fields` and `documents`. Roles with `can_skip: false` in the schema are mandatory. For a UBO, pass the ownership share in `share_size`.

**How errors are handled**

* An error in a row marks **only that row** as `invalid`; all other rows are processed.
* If the manifest as a whole cannot be accepted, the commit is rejected with `422` and the batch stays editable. This happens when `manifest.json` is missing or too large, is not valid JSON, has trailing content, has no items, has more than **10,000 rows**, or has a foreign `schema_version`.

***

**Row statuses**

| Status             | Meaning                                                                                                              |
| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `invalid`          | The row failed manifest validation. The reason is in `error`.                                                        |
| `valid`            | Passed validation, waiting to be processed.                                                                          |
| `creating`         | The customer is being created and the documents submitted.                                                           |
| `submitted`        | Submitted for compliance review.                                                                                     |
| `finished`         | Customer approved. Final.                                                                                            |
| `rejected`         | Customer rejected. Final.                                                                                            |
| `change_requested` | Reviewers ask to correct data or documents.                                                                          |
| `failed`           | Could not be processed: broken file, duplicate customer or another permanent error. The reason is in `error`. Final. |

Batch counters are always numbers, including zeros. Uploading files alone does not change them; before the first commit all counters are `0`.

**Where to find rejection details**

* `GET .../status` returns `problems`: rows in `invalid`, `failed`, `change_requested` or `rejected`, each with a readable `summary`.
* `GET .../items/{row_key}` returns the full `review` object, including rejection reasons per document (`document_reject_reasons`), per associated party (`party_reject_reasons`) and per verification step (`step_reject_reasons`).

***

**Corrections**

A typical situation: some rows ended up `invalid` or `change_requested`.

1. Reopen the batch: `POST /v2/compliance/batch/{batch_id}/change`. Status becomes `CHANGE_REQUESTED`.
2. Get new credentials and upload the corrected files. Put a replacement document under a **new path**; do not overwrite the old file.
3. Upload the **full** manifest again. The manifest is always the source of truth: a row you remove that has not yet been submitted for review is withdrawn.
4. Commit again.

**Rules for existing rows**

* Rows in a final state (`finished`, `rejected`, `failed`) are never changed by a later commit.
* To retry a customer whose row is `failed`, add a new row with a new `row_key`.
* The `type` and `external_customer_id` of an existing `row_key` cannot be changed.

**Letting the customer fix it themselves**

For a row in `change_requested`, you can request a fresh verification link and send it to the end customer:

```bash
GET /v2/compliance/batch/{batch_id}/items/{row_key}/web_sdk_link
```

The response contains `url` and `ttl_seconds`. For rows in any other status the call returns `409`.

***

**Cancelling a batch**

`POST /v2/compliance/batch/{batch_id}/cancel` closes the batch permanently and frees a slot in the open-batch limit. It works for `OPEN` and `CHANGE_REQUESTED` batches.

* Rows that were not yet submitted for review become `failed`.
* Rows already submitted cannot be withdrawn and will still receive a result.

***

**Errors**

Errors are returned in the following format:

```json
{
  "code": 9,
  "message": "batch cannot be committed from its current state",
  "details": []
}
```

`code` is a gRPC status code; use the HTTP status of the response for your error handling.

| HTTP  | When                                                                                                                                                         |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400` | Invalid request (e.g. empty or repeated `types`, unsupported `format` value).                                                                                |
| `403` | Your partner account is not enabled for Bulk Client Import, or the token lacks the required scope.                                                           |
| `404` | The batch does not exist or belongs to another partner.                                                                                                      |
| `409` | The action is not allowed in the batch's current status, the open-batch limit is reached, or a WebSDK link is requested for a row not in `change_requested`. |
| `422` | The manifest was rejected at commit. The batch stays editable.                                                                                               |
| `503` | The verification schema source is temporarily unavailable when creating a batch. Retry later.                                                                |

***

**Best practices**

* **Start from the example manifest** instead of building rows by hand.
* **Always send `external_customer_id`** so a customer is never created twice, even across batches.
* **Upload everything before committing**, and run uploads and commits sequentially, not in parallel.
* **Retry a commit only after checking the batch status.** A repeated commit on a `CLOSED` batch returns `200` but does not accept a new manifest.
* **Use `processed`, not `done`, as your onboarding milestone.** `done` also waits for compliance decisions, which can take longer.
* **Poll with reasonable intervals.** Rows move asynchronously; there is no need to poll more often than once every few minutes.

***

**Endpoint reference**

| Method                 | Endpoint                                                           | Description                                        |
| ---------------------- | ------------------------------------------------------------------ | -------------------------------------------------- |
| List Batches           | `GET /v2/compliance/batch`                                         | List your batches.                                 |
| Create Batch           | `POST /v2/compliance/batch`                                        | Create a batch and freeze its schema.              |
| Get Batch              | `GET /v2/compliance/batch/{batch_id}`                              | Batch status, counters and flags.                  |
| Get Batch Schema       | `GET /v2/compliance/batch/{batch_id}/schema`                       | Frozen schema snapshot.                            |
| Get Example Manifest   | `GET /v2/compliance/batch/{batch_id}/example-manifest`             | Manifest template with placeholders.               |
| Get Upload Credentials | `GET /v2/compliance/batch/{batch_id}/credentials`                  | Temporary storage credentials.                     |
| Commit Batch           | `POST /v2/compliance/batch/{batch_id}/commit`                      | Start processing.                                  |
| Request Batch Changes  | `POST /v2/compliance/batch/{batch_id}/change`                      | Reopen a committed batch for corrections.          |
| Cancel Batch           | `POST /v2/compliance/batch/{batch_id}/cancel`                      | Close a batch permanently.                         |
| List Batch Items       | `GET /v2/compliance/batch/{batch_id}/items`                        | List rows with a status filter.                    |
| Get Batch Item         | `GET /v2/compliance/batch/{batch_id}/items/{row_key}`              | One row with review details.                       |
| Get Item WebSDK Link   | `GET /v2/compliance/batch/{batch_id}/items/{row_key}/web_sdk_link` | Verification link for a row in `change_requested`. |
| Get Batch Status       | `GET /v2/compliance/batch/{batch_id}/status`                       | Problems summary and created customers.            |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.delos.financial/batchservice/bulk-client-import.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
