Onboard a business and its owners
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
- 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.
- Once the business is verified, its standard limit is 50,000.00 USD a day. A business never goes above
basic. - 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:createandcustomer:update.
Find out what is required
- The create fields are fixed. A 400 response names the field.
- The company document set is fixed:
COMPANY_DOCfour times, with sub-typesINCORPORATION_CERT,SHAREHOLDER_REGISTRY,PROOF_OF_ADDRESSandTAX_DOCUMENT(orOTHER). - 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
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"
}
}
]
}
'
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());
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())
{
"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 (the link goes to Tunde) or 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:
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"
}
}
'
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());
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())
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
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"
}
'
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());
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())
{
"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}. 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.
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"
}
}
]
}
'
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());
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())
{
"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
customer.createdtwice, once for the business and once for Tunde (typeubo).- Tunde's
customer.kyc.status.pending, then.completed. - The business's
customer.kyc.status.pending, then.completed, or.failedwithfailure_reason.
Deduplicate webhooks on the body's event_id. See Customer and KYC events.
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.