> ## Documentation Index
> Fetch the complete documentation index at: https://docs.business.blaaiz.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Onboarding a business customer

> End-to-end walkthrough for creating, populating, and submitting a business-type customer for KYB.

This guide walks through onboarding a `type=business` customer from creation to verification. Read [Business customer KYB](/guides/kyb/overview) first if you haven't — it explains the lifecycle, the `kyb_scope` model (Standard / FULL vs Minimal), and the rules that this walkthrough assumes.

The **first six steps cover Standard (FULL) onboarding** — the default flow, which produces a customer that can hold an NGN, USD, GBP, or EUR Virtual Bank Account. The [Minimal KYB onboarding](#minimal-kyb-onboarding) section at the end is the abbreviated NGN-only flow for whitelisted merchants. If you're not sure which one applies to you: you almost certainly want Standard.

## Prerequisites

* An OAuth access token with `customer:write` and `customer:read` scopes — see [Authentication](/guides/authentication).
* The business's basic profile information (name, registration number, country of incorporation, etc.).
* Each beneficial owner's personal details and a clear ID document image.
* The company's formation document (Certificate of Incorporation or equivalent).

## Step 1: Create the customer

Create the customer with `type=business`. Beneficial owners are required for verification (see [Readiness](/guides/kyb/overview#what-we-check-before-accepting-a-submit)) but you don't have to include them on this initial call — you can attach them now via the `owners` array (up to 5 entries) or add them later via `PUT /api/external/customer/{id}`. Either way, every owner that exists at `/submit` time must satisfy the readiness checks.

```bash theme={null}
curl -X POST https://api-prod.blaaiz.com/api/external/customer \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "business",
    "business_name": "Acme Corp",
    "trading_name": "Acme",
    "business_type": "llc",
    "registration_number": "RC123456",
    "incorporation_country": "NG",
    "country": "NG",
    "incorporation_date": "2019-03-12",
    "industry_type": "Software & SaaS",
    "business_description": "B2B SaaS platform for African logistics operators.",
    "website": "https://acme.example.com",
    "source_of_funds": "business_revenue",
    "estimated_annual_revenue": "1000000_4999999",
    "expected_monthly_payments": 250,
    "account_purpose": "send_receive_funds_related_parties",
    "email": "contact@acme.example.com",
    "phone": "+2348012345678",
    "tin": "12345678-0001",
    "owners": [
      {
        "first_name": "Jane",
        "last_name": "Doe",
        "ownership_percentage": 60,
        "is_beneficial_owner": true,
        "is_signer": true,
        "has_control": true
      },
      {
        "first_name": "John",
        "last_name": "Smith",
        "ownership_percentage": 40,
        "is_beneficial_owner": true
      }
    ]
  }'
```

<Note>
  For business customers the canonical identifier is `registration_number` + `incorporation_country`, not the legacy `id_type` / `id_number` fields. Both `registration_number` and `incorporation_country` are required at create time. The actual certificate-of-incorporation **file** goes into the KYB document collection in step 4 — never through `/files`.

  **Two countries, two addresses.** A business customer carries up to three ISO country codes:

  * `incorporation_country` — the legal jurisdiction the company is registered in. Top-level, business-only, required.
  * `country` — the registered-address country. Top-level. For businesses, must equal `incorporation_country` (a company's registered office sits in its country of incorporation by company law). The cross-field check is enforced at validation time — sending divergent values fails the request with `422`. When changing the jurisdiction on update, send both fields together.
  * `operating_country` — the operating-address country, can differ from the other two. Lives on KYC data alongside the rest of the `operating_*` fields. Required whenever any other `operating_*` field is supplied (so each address is self-describing).

  Postal-code validation is anchored per address: `zip_code` against `country`, `operating_zip_code` against `operating_country`.

  **Do not send `id_type` or `id_number` on a business customer.** Those columns are individual-only — they identify a person's passport / driver's license / resident permit. Businesses identify via `registration_number` + `incorporation_country`. Sending `id_type` or `id_number` on a business create or update returns `422`.
</Note>

Save the returned `id` as your `customer_id`. The `verification_status` is `PENDING`.

## Step 2: Add KYC data (operating address, etc.)

If you have additional address or KYC fields to capture — including the operating address when it differs from the registered address — call `POST /api/external/customer/{id}/kyc-data`:

```bash theme={null}
curl -X POST https://api-prod.blaaiz.com/api/external/customer/CUSTOMER_ID/kyc-data \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "street": "456 Business Ave",
    "city": "Lagos",
    "state": "Lagos",
    "zip_code": "100001",
    "operating_street": "789 Operations Blvd",
    "operating_city": "Lagos",
    "operating_state": "Lagos",
    "operating_zip_code": "100002",
    "operating_country": "NG"
  }'
```

`operating_country` anchors the postal-code validation on `operating_zip_code` — if you send any of the `operating_*` fields and `operating_country` is not yet persisted on the row, you must include it.

This step is optional — fields you didn't capture at creation can also be patched via `PUT /api/external/customer/{id}` later.

## Step 3: Upload an ID file for each owner

Each owner needs at least their `id_document_front`. For two-sided IDs (e.g. driver's license) include `id_document_back`. The flow is two steps per file: get a presigned URL, PUT the file to S3, then register the upload.

### 3.1 Get a presigned URL

```bash theme={null}
curl -X POST \
  https://api-prod.blaaiz.com/api/external/customer/CUSTOMER_ID/owner/OWNER_ID/file/presigned-url \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "file_category": "id_document_front" }'
```

The response gives you `file_id`, `url` (a 5-minute presigned S3 URL), and `headers`.

### 3.2 PUT the file to S3

```bash theme={null}
curl -X PUT "$URL" \
  -H "x-amz-acl: private" \
  -H "Content-Type: application/pdf" \
  --data-binary @owner-jane-passport.pdf
```

Max 25 MB. Allowed MIME: `application/pdf`, `image/jpeg`, `image/png`. The file must be clear, legible, and authentic — see [General guidelines](/guides/general-guidelines) for the document quality rules. Fraudulent documents result in permanent blacklisting.

### 3.3 Register the upload

```bash theme={null}
curl -X POST \
  https://api-prod.blaaiz.com/api/external/customer/CUSTOMER_ID/owner/OWNER_ID/files \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "id_document_front": "FILE_ID" }'
```

Repeat 3.1–3.3 for each owner. If the owner has a two-sided ID, do the back side as a separate `file_category=id_document_back` round.

## Step 4: Register KYB documents

The company's formation document and any other corporate paperwork (articles, share register, bank statements, proof of address, etc.) go into the KYB document collection. Same two-step pattern.

### 4.1 Get a presigned URL

```bash theme={null}
curl -X POST \
  https://api-prod.blaaiz.com/api/external/customer/CUSTOMER_ID/document/presigned-url \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

Response gives you `file_id` and a 5-minute `url`.

### 4.2 PUT the file to S3

Same as 3.2.

### 4.3 Register the document

```bash theme={null}
curl -X POST \
  https://api-prod.blaaiz.com/api/external/customer/CUSTOMER_ID/document \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "CERTIFICATE_OF_INCORPORATION",
    "name": "Acme Corp — Certificate of Incorporation.pdf",
    "file_id": "FILE_ID",
    "description": "Filed 2019-03-12, Companies House"
  }'
```

You must register at least one document of a formation type before `/submit` will accept the customer. Formation types are `CERTIFICATE_OF_INCORPORATION`, `ARTICLES_OF_INCORPORATION`, `BENEFICIAL_OWNERSHIP_CERTIFICATE`, `INCORPORATION_DOCUMENTS`, and `CAC_STATUS_REPORT`. Compliance may require additional document types depending on the business profile — review feedback comes through `admin_comments` on the customer-level rejection.

## Step 5: Submit for verification

```bash theme={null}
curl -X POST \
  https://api-prod.blaaiz.com/api/external/customer/CUSTOMER_ID/submit \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

The customer flips to `PROCESSING`. All `PENDING` owners and documents flip to `PROCESSING` along with it. The customer-level fields, every owner, and every document are now locked from your edits until the customer-level webhook arrives.

If the readiness check fails, the response is `422` with a message naming the specific gap. The full list of preconditions is in [Readiness](/guides/kyb/overview#what-we-check-before-accepting-a-submit). Common cases you'll hit:

| Symptom                                                                      | Where to fix it                                                                                                                                                                          |
| :--------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "at least one owner is required"                                             | Add an owner (step 1 or `PUT /customer/{id}` with the `owners` array).                                                                                                                   |
| "ownership\_percentage of all owners must sum to 100%"                       | Adjust the percentages across all owners so they total 100.                                                                                                                              |
| "at least one formation document is required"                                | Register one with a formation type — step 4.                                                                                                                                             |
| "owner ... is missing id\_document\_front"                                   | Run step 3 for that owner.                                                                                                                                                               |
| "owner ... id\_document\_type=drivers\_license requires id\_document\_back"  | Add the back-side upload — re-run step 3 with `file_category=id_document_back`.                                                                                                          |
| "owner ... id\_document\_type=passport must not have id\_document\_back"     | The owner has a stale back image from a previous `id_document_type`. Either change the type back to one that requires it, or contact support to clear the file.                          |
| "owner ... date\_of\_birth must be at least 18 years ago"                    | Update the owner's DOB.                                                                                                                                                                  |
| "owner ... id\_expiry\_date must be a future date"                           | Replace the ID with a current document and update `id_expiry_date`.                                                                                                                      |
| "owner emails must be unique within the customer"                            | Two owners share an email. Update one of them.                                                                                                                                           |
| "business\_name / registration\_number / incorporation\_country is required" | The field was cleared since creation. Set it via `PUT /customer/{id}`.                                                                                                                   |
| "incorporation\_country must be a valid ISO 3166-1 alpha-2 country code"     | Use a real ISO code (e.g. `NG`, `US`, `GB`).                                                                                                                                             |
| "country must equal incorporation\_country"                                  | The two diverged. Update both together via `PUT /customer/{id}` — `{"country": "GB", "incorporation_country": "GB"}` — so the registered-address country matches the legal jurisdiction. |

## Step 6: Wait for the webhook

You will receive a `customer.status_changed` webhook on your `collection_url` when the customer transitions to `VERIFIED` or `REJECTED`. Business KYB review typically takes 1–5 business days. **Do not poll, do not raise tickets within that window** — see [General guidelines](/guides/general-guidelines#verification-timeline).

If `VERIFIED`, the customer is ready to use — for an NGN Virtual Bank Account, an NGN payout, or any other operation that requires a verified customer.

If `REJECTED`, every owner and document that needed fixing will have entries in its `admin_comments` array explaining what's wrong. See [Handling KYB rejections](/guides/kyb/handling-rejections).

## Common mistakes

These are the patterns we see most often that cause grief.

**1. Calling `POST /customer/{id}/files` (or `POST /file/get-presigned-url`) for a business customer.**
Both endpoints reject business customers with `400` — they're individual-only. Symptom: you can't even upload. Fix: use the dedicated owner-file and KYB-document endpoints from steps 3 and 4.

**2. Ownership percentages that don't sum to 100.**
`/submit` enforces this strictly. 99.99% won't do. Add up the values across every owner and make sure they total exactly 100.

**3. Trying to edit a customer (or its owners / documents) while the customer is `PROCESSING` or `VERIFIED`.**
The API returns 400. `PROCESSING` is the active-review window — wait for the customer-level webhook before attempting any edits. If the customer comes back `REJECTED`, parent + child edits unlock and you can fix the issues. For corrections on a `VERIFIED` customer, see [Customer data corrections](/guides/customer-data-corrections).

**4. Sending `status` or `admin_comments` in your update payloads.**
Those fields are admin-managed. They're silently stripped from your requests — your owner stays at `PENDING` and your comments don't get saved. Don't rely on them appearing on the response if you sent them.

**5. Forgetting `registration_number` and `incorporation_country` at create time.**
These two are required for `type=business` and they're how the business is indexed in our compliance system. The legacy `id_type` / `id_number` are **prohibited** on business customers — sending either returns `422`.

**6. Sending `incorporation_country` without `country` (or vice versa) on update.**
For business customers, `country` (registered-address country) must equal `incorporation_country` (legal jurisdiction). When you change the jurisdiction, update both fields in the same `PUT` payload. Updating only one leaves the other at its persisted value, which produces a divergent pair and a `422`. Sending different values for the two also returns `422`.

**7. Sending `operating_*` fields without `operating_country`.**
Each address has its own country anchor: `zip_code` validates against `country`, `operating_zip_code` validates against `operating_country`. At create time, sending any `operating_*` field requires `operating_country`. At update time, the persisted value is used as a fallback — but if you've never set it, the request fails.

**8. Treating `customer.created` as "ready to transact."**
`customer.created` fires the moment you POST `/customer`. Verification has not even started yet. Wait for `customer.status_changed` with `new_status=VERIFIED`.

## Minimal KYB onboarding

<Note>
  This section only applies if your platform has been **placed on the Minimal KYB allow list by Blaaiz**. If you haven't been told you're on the allow list, you aren't — sending `kyb_scope=MINIMAL` returns 422 with a "not configured for minimal KYB" message. Read [Two onboarding paths](/guides/kyb/overview#two-onboarding-paths-standard-and-minimal) for context.
</Note>

The Minimal flow is for platforms whose business customers only need NGN Virtual Bank Accounts. It's the same shape as the Standard flow above — business name, country, registration number, country of incorporation, formation document — **but you don't have to provide beneficial owners**. Customers onboarded this way can hold NGN VBAs and run NGN payouts; they cannot hold USD / GBP / EUR VBAs unless upgraded.

The data you send is a strict subset of Standard. So the steps you skip are: (3) per-owner ID file uploads, and the owners array on the create payload.

### Step 1 (Minimal): Create the customer

```bash theme={null}
curl -X POST https://api-prod.blaaiz.com/api/external/customer \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "business",
    "kyb_scope": "MINIMAL",
    "business_name": "Acme Corp",
    "email": "contact@acme.example.com",
    "country": "NG",
    "incorporation_country": "NG",
    "registration_number": "RC-123456",
    "phone": "+2348012345678"
  }'
```

What's required:

* `type=business`
* `kyb_scope=MINIMAL` (or omit it — allow-listed platforms default to MINIMAL)
* `business_name`
* `email`
* `country` (ISO alpha-2)
* `incorporation_country` (ISO alpha-2; must equal `country`)
* `registration_number` — the company's registration / RC number

What's **not** required:

* Beneficial owners (owners array)
* Per-owner ID files
* Operating address

Save the returned `id` as your `customer_id`.

### Step 2 (Minimal): Register the formation document

This step is mandatory and identical to [Step 4 of the Standard flow](#step-4-register-kyb-documents): get a presigned URL, PUT the file to S3, then register it via `POST /customer/{id}/document` with `type=CERTIFICATE_OF_INCORPORATION`.

### Step 3 (Minimal): Submit for verification

```bash theme={null}
curl -X POST \
  https://api-prod.blaaiz.com/api/external/customer/CUSTOMER_ID/submit \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

If your platform has `auto_verify_business_customers` enabled, the customer goes straight to `VERIFIED`. Otherwise it goes to `PROCESSING` and compliance reviews it (typically within 1 business day for the Minimal data set, since there's less to review).

You're done — the customer can now have an NGN VBA created.

## Upgrading from Minimal to Full

If a Minimal customer later needs to hold a USD / GBP / EUR VBA, you upgrade them to Full by adding beneficial-owner data:

```bash theme={null}
curl -X POST \
  https://api-prod.blaaiz.com/api/external/customer/CUSTOMER_ID/upgrade-kyb-scope \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "owners": [
      {
        "first_name": "Jane",
        "last_name": "Doe",
        "ownership_percentage": 60,
        "is_beneficial_owner": true,
        "is_signer": true,
        "has_control": true
      },
      {
        "first_name": "John",
        "last_name": "Smith",
        "ownership_percentage": 40,
        "is_beneficial_owner": true
      }
    ]
  }'
```

What this does:

* Saves the new owners on the customer record.
* Flips `kyb_scope` to `FULL`.
* If the customer was `VERIFIED`, transitions them back to `PENDING`.

`registration_number` and `incorporation_country` are already on the customer from Minimal onboarding, so you don't need to send them again. (You can if you want to update them, but typically you don't.)

After the upgrade, upload owner ID files via the normal owner-file endpoints (Step 3 of the Standard flow), and call `/submit` to re-verify under the Standard checks.

**One-way only** — there is no downgrade. If you're not sure whether to onboard a customer at Minimal or Full, default to Full (or whichever scope your default behavior produces): you can always add owners and re-submit, but you can't remove the Full data once it's on file.
