---
title: "Onboard a business and its owners"
description: "Create a business with its directors and owners, verify each person, upload the four company documents, and verify the business."
---
This recipe creates a business with its directors and owners, verifies each person, uploads the four company documents and verifies the business.

**Who it is for.** A B2B payments platform that onboards Adire Logistics Ltd, a Lagos freight forwarder. Its director, Tunde Bakare, owns 60% of it.

**Status.** Available, by hosted page or by documents you upload. In sandbox the review does not complete today.

## Money example

1. You create Adire Logistics with Tunde as an associated person. Three steps follow, in order. You verify Tunde, you upload the company documents, and then you verify the business.
2. Once the business is verified, its standard limit is 50,000.00 USD a day. A business never goes above `basic`.
3. Adire can then open a USD account and receive a 12,000.00 USD freight payment into it.

## Before you start

- A company email domain. Gravv refuses these free-mail domains: gmail.com, yahoo.com, outlook.com, aol.com, hotmail.com, icloud.com, protonmail.com.
- For **each** associated person, you need names, an email of their own (not the business email, and not used by any other customer), gender, date of birth and a full address. Phone is optional when you create the business.
- The company documents as files: certificate of incorporation, shareholder register, proof of address and a tax document.
- A scoped key needs `customer:create` and `customer:update`.

## Find out what is required

- The create fields are fixed. A 400 response names the field.
- The company document set is fixed: `COMPANY_DOC` four times, with sub-types `INCORPORATION_CERT`, `SHAREHOLDER_REGISTRY`, `PROOF_OF_ADDRESS` and `TAX_DOCUMENT` (or `OTHER`).
- The call that starts business verification returns a 400 that names any person who is not verified, or lists the documents still missing. Use this response as the check.

## Steps

### 1. Create the business and its people

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/customers \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 7f4c1c60-4e8c-4dca-b43a-feb4df9dc0e6' \
     --header 'Content-Type: application/json' \
     --data '
{
  "type": "business",
  "email": "finance@adirelogistics.example",
  "phone": "+2348091234567",
  "business_name": "Adire Logistics Ltd",
  "business_description": "Freight forwarding and customs clearing for importers",
  "business_type": "private_limited",
  "business_industry": "Transportation and Warehousing",
  "registration_number": "RC-1234567",
  "external_id": "biz-2040",
  "address": {
    "address_line1": "22 Creek Road",
    "city": "Apapa",
    "state": "NG-LA",
    "postal_code": "102272",
    "country": "NG"
  },
  "associated_persons": [
    {
      "first_name": "Tunde",
      "last_name": "Bakare",
      "email": "tunde.bakare@adirelogistics.example",
      "phone": "+2348021234567",
      "gender": "male",
      "date_of_birth": "1984-07-22",
      "address": {
        "address_line1": "7 Glover Road",
        "city": "Ikoyi",
        "state": "NG-LA",
        "postal_code": "106104",
        "country": "NG"
      }
    }
  ]
}
'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/customers", {
  method: "POST",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
    "Idempotency-Key": "7f4c1c60-4e8c-4dca-b43a-feb4df9dc0e6",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "type": "business",
    "email": "finance@adirelogistics.example",
    "phone": "+2348091234567",
    "business_name": "Adire Logistics Ltd",
    "business_description": "Freight forwarding and customs clearing for importers",
    "business_type": "private_limited",
    "business_industry": "Transportation and Warehousing",
    "registration_number": "RC-1234567",
    "external_id": "biz-2040",
    "address": {
      "address_line1": "22 Creek Road",
      "city": "Apapa",
      "state": "NG-LA",
      "postal_code": "102272",
      "country": "NG"
    },
    "associated_persons": [
      {
        "first_name": "Tunde",
        "last_name": "Bakare",
        "email": "tunde.bakare@adirelogistics.example",
        "phone": "+2348021234567",
        "gender": "male",
        "date_of_birth": "1984-07-22",
        "address": {
          "address_line1": "7 Glover Road",
          "city": "Ikoyi",
          "state": "NG-LA",
          "postal_code": "106104",
          "country": "NG"
        }
      }
    ]
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.post(
    "https://api.gravv.xyz/v1/customers",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
        "Idempotency-Key": "7f4c1c60-4e8c-4dca-b43a-feb4df9dc0e6",
        "Content-Type": "application/json",
    },
    json={
        "type": "business",
        "email": "finance@adirelogistics.example",
        "phone": "+2348091234567",
        "business_name": "Adire Logistics Ltd",
        "business_description": "Freight forwarding and customs clearing for importers",
        "business_type": "private_limited",
        "business_industry": "Transportation and Warehousing",
        "registration_number": "RC-1234567",
        "external_id": "biz-2040",
        "address": {
            "address_line1": "22 Creek Road",
            "city": "Apapa",
            "state": "NG-LA",
            "postal_code": "102272",
            "country": "NG",
        },
        "associated_persons": [
            {
                "first_name": "Tunde",
                "last_name": "Bakare",
                "email": "tunde.bakare@adirelogistics.example",
                "phone": "+2348021234567",
                "gender": "male",
                "date_of_birth": "1984-07-22",
                "address": {
                    "address_line1": "7 Glover Road",
                    "city": "Ikoyi",
                    "state": "NG-LA",
                    "postal_code": "106104",
                    "country": "NG",
                },
            },
        ],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response, trimmed"
{
  "data": {
    "id": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "type": "business",
    "status": "active",
    "business_name": "Adire Logistics Ltd",
    "industry_code": "488999",
    "associated_persons": [
      {
        "id": "5e4d3c2b-1a0f-4e9d-8c7b-6a5f4e3d2c1b",
        "first_name": "Tunde",
        "last_name": "Bakare",
        "type": "ubo",
        "status": "active"
      }
    ]
  },
  "error": null
}
```

Save the business `id` **and every `associated_persons[].id`**. You verify each person with their own id. `industry_code` comes from an exact match on `business_industry`, and any other text gets `999999`.

### 2. Verify each person

Use [the hosted page](/recipes/customers-and-kyc/verify-a-person-on-a-hosted-page#3-start-the-hosted-check) (the link goes to Tunde) or [documents you upload](/recipes/customers-and-kyc/verify-a-person-with-documents-you-upload), with `customer_id` `5e4d3c2b-1a0f-4e9d-8c7b-6a5f4e3d2c1b`. Wait until his `basic` row is `completed`. To check, read `associated_persons[].kyc` on the business, or wait for his own `customer.kyc.status.completed`.

### 3. Upload the four company documents

Send one call per document, each with a new `Idempotency-Key`. The first call:

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/customers/kyc/upload-document \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 1766ab8a-8274-4bed-bb0c-bf2c672d5401' \
     --header 'Content-Type: application/json' \
     --data '
{
  "customer_id": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
  "filename": "certificate-of-incorporation.pdf",
  "content": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0",
  "metadata": {
    "idDocType": "COMPANY_DOC",
    "idDocSubType": "INCORPORATION_CERT",
    "country": "NG"
  }
}
'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/customers/kyc/upload-document", {
  method: "POST",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
    "Idempotency-Key": "1766ab8a-8274-4bed-bb0c-bf2c672d5401",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "customer_id": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "filename": "certificate-of-incorporation.pdf",
    "content": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0",
    "metadata": {
      "idDocType": "COMPANY_DOC",
      "idDocSubType": "INCORPORATION_CERT",
      "country": "NG"
    }
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.post(
    "https://api.gravv.xyz/v1/customers/kyc/upload-document",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
        "Idempotency-Key": "1766ab8a-8274-4bed-bb0c-bf2c672d5401",
        "Content-Type": "application/json",
    },
    json={
        "customer_id": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
        "filename": "certificate-of-incorporation.pdf",
        "content": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0",
        "metadata": {
            "idDocType": "COMPANY_DOC",
            "idDocSubType": "INCORPORATION_CERT",
            "country": "NG",
        },
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

Then send the same call with `idDocSubType` `SHAREHOLDER_REGISTRY`, `PROOF_OF_ADDRESS` and `TAX_DOCUMENT`. Each response is 200 with `document_type` `COMPANY_DOC` and the sub-type. Upload a person's documents under the **person's** id, never the business's.

### 4. Verify the business

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/customers/kyc/start-s2s \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 56c36516-f70b-48eb-b1e6-7b61a3a7638a' \
     --header 'Content-Type: application/json' \
     --data '
{
  "customer_id": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d"
}
'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/customers/kyc/start-s2s", {
  method: "POST",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
    "Idempotency-Key": "56c36516-f70b-48eb-b1e6-7b61a3a7638a",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "customer_id": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d"
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.post(
    "https://api.gravv.xyz/v1/customers/kyc/start-s2s",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
        "Idempotency-Key": "56c36516-f70b-48eb-b1e6-7b61a3a7638a",
        "Content-Type": "application/json",
    },
    json={
        "customer_id": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "status": "pending",
    "message": "KYB verification initiated. Status will be updated via webhook."
  },
  "error": null
}
```

Instead, you can call `POST /v1/customers/kyc/start` with the business id to get a hosted link, valid for 1 hour, where a company officer completes the company check.

### 5. Wait for the result

Wait for `customer.kyc.status.completed` on the business. Its `event_data` adds `business_name`, `business_type` and `registration_number`. Then `kyc[type=basic].status` on the business is `completed`.

### 6. Add a person later

To add a new director, send `associated_persons` with **no `id`** on [`PUT /v1/customers/{customer_id}`](/api-reference/customers/put-v1-customers-customer-id). PUT takes no `Idempotency-Key`. Each new person needs first and last name, email, date of birth, gender, **phone** and address. Gravv sends `customer.created` for the new person, and you must verify the new person before you start any new business verification.

<CodeGroup>
```bash title="cURL"
curl --request PUT \
     --url https://api.gravv.xyz/v1/customers/0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d \
     --header 'Api-Key: <API_KEY>' \
     --header 'Content-Type: application/json' \
     --data '
{
  "associated_persons": [
    {
      "first_name": "Ngozi",
      "last_name": "Eze",
      "email": "ngozi.eze@adirelogistics.example",
      "phone": "+2348051234567",
      "gender": "female",
      "date_of_birth": "1989-02-11",
      "address": {
        "address_line1": "3 Awolowo Road",
        "city": "Ikoyi",
        "state": "NG-LA",
        "postal_code": "106104",
        "country": "NG"
      }
    }
  ]
}
'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/customers/0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d", {
  method: "PUT",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "associated_persons": [
      {
        "first_name": "Ngozi",
        "last_name": "Eze",
        "email": "ngozi.eze@adirelogistics.example",
        "phone": "+2348051234567",
        "gender": "female",
        "date_of_birth": "1989-02-11",
        "address": {
          "address_line1": "3 Awolowo Road",
          "city": "Ikoyi",
          "state": "NG-LA",
          "postal_code": "106104",
          "country": "NG"
        }
      }
    ]
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.put(
    "https://api.gravv.xyz/v1/customers/0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
        "Content-Type": "application/json",
    },
    json={
        "associated_persons": [
            {
                "first_name": "Ngozi",
                "last_name": "Eze",
                "email": "ngozi.eze@adirelogistics.example",
                "phone": "+2348051234567",
                "gender": "female",
                "date_of_birth": "1989-02-11",
                "address": {
                    "address_line1": "3 Awolowo Road",
                    "city": "Ikoyi",
                    "state": "NG-LA",
                    "postal_code": "106104",
                    "country": "NG",
                },
            },
        ],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "message": "customer updated successfully",
    "status": true
  },
  "error": null
}
```

## What can go wrong

| Response | Why | Fix |
| :-- | :-- | :-- |
| 400 `only company emails are allowed` | The business email uses a free-mail domain | Use the company domain |
| 400 `at least one associated person is required for a business customer` | No `associated_persons` | Add the directors and owners |
| 400 `failed to add customer: associated person emails must be unique` | Two persons share an email, or one uses the business email | Give each person their own email |
| 400 `failed to add customer: associated person email already exists` | The email belongs to another customer in your business | Use another email. Two businesses cannot share a person by email |
| 400 `associated person address fields (line1, city, postal_code, state, country) are required` | A person has no full address | Add the full address |
| 400 `business name cannot contain special characters` | Characters outside letters, digits, spaces and `'&,.()-` | Remove them |
| 400 `INVALID_REQUEST: associated person "Tunde Bakare" must complete basic KYC before business verification can proceed` | A person is not verified yet | Finish step 2, then start again with a new key |
| 400 `INVALID_REQUEST: missing required business documents: Ownership/Shareholder Registry (SHAREHOLDER_REGISTRY), Tax Document (TAX_DOCUMENT)` | The company documents are incomplete | Upload the named sub-types |
| 400 `INVALID_REQUEST: document type "PASSPORT" is not allowed for business customers; allowed type: COMPANY_DOC` | You uploaded a person's document under the business id | Upload it with the person's id |
| 400 `INVALID_REQUEST: metadata.idDocSubType is required for business document uploads (…)` | The sub-type is missing | Add the sub-type |
| `REGISTRATION_CERTIFICATE` refused | The value came from an old example, and Gravv has never accepted it | Use `COMPANY_DOC` with `INCORPORATION_CERT` |
| PUT 400 `associated person phone is required for new persons` | A new person has no phone | Add the phone |
| PUT 400 `associated person does not belong to this customer` | The person id belongs to another business | Use the right business id |
| 500 `KYC processing failed. Please try again later or contact support.` | Gravv could not register the company for review | Retry later, and contact support if it persists |

## Webhooks you will receive

1. `customer.created` twice, once for the business and once for Tunde (`type` `ubo`).
2. Tunde's `customer.kyc.status.pending`, then `.completed`.
3. The business's `customer.kyc.status.pending`, then `.completed`, or `.failed` with `failure_reason`.

Deduplicate webhooks on the body's `event_id`. See [Customer and KYC events](/platform/webhooks/event-catalogue#customers-and-kyc).

## Testing in sandbox

The create, the PUT that adds a person, the four uploads and the persons-first rule all work with a `grvSec_sandbox_` key. You cannot verify the business in sandbox today. The persons' reviews never reach `completed`, so the call that starts business verification returns the persons-first 400.

## Reference

- [Create a customer](/api-reference/customers/post-v1-customers)
- [Update a customer](/api-reference/customers/put-v1-customers-customer-id)
- [Upload a KYC document](/api-reference/kyc/post-v1-customers-kyc-upload-document)
- [Initiate server-to-server verification](/api-reference/kyc/post-v1-customers-kyc-start-s2s)
- [Start KYC verification](/api-reference/kyc/post-v1-customers-kyc-start)
