curl --request POST \
--url https://sandbox-api.sakawave.example/v1/customers \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "individual",
"jurisdiction": "NG",
"externalReference": "cust_1001",
"email": "ada@example.com",
"phoneNumber": "+2348012345678",
"address": {
"line1": "14 Marina Road",
"line2": null,
"city": "Lagos",
"state": "Lagos",
"postalCode": "100001",
"country": "NG"
},
"firstName": "Ada",
"lastName": "Okafor",
"bvn": "22212345678"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'individual',
jurisdiction: 'NG',
externalReference: 'cust_1001',
email: 'ada@example.com',
phoneNumber: '+2348012345678',
address: {
line1: '14 Marina Road',
line2: null,
city: 'Lagos',
state: 'Lagos',
postalCode: '100001',
country: 'NG'
},
firstName: 'Ada',
lastName: 'Okafor',
bvn: '22212345678'
})
};
fetch('https://sandbox-api.sakawave.example/v1/customers', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://sandbox-api.sakawave.example/v1/customers"
payload = {
"type": "individual",
"jurisdiction": "NG",
"externalReference": "cust_1001",
"email": "ada@example.com",
"phoneNumber": "+2348012345678",
"address": {
"line1": "14 Marina Road",
"line2": None,
"city": "Lagos",
"state": "Lagos",
"postalCode": "100001",
"country": "NG"
},
"firstName": "Ada",
"lastName": "Okafor",
"bvn": "22212345678"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "01K2Z3AB4C5D6E7F8G9HJKMNPQ",
"type": "individual",
"jurisdiction": "NG",
"externalReference": "cust_1001",
"displayName": "Ada Okafor",
"name": null,
"firstName": "Ada",
"middleName": null,
"lastName": "Okafor",
"email": "ada@example.com",
"phoneNumber": "+2348012345678",
"dateOfBirth": "1992-04-18",
"gender": "female",
"address": {
"line1": "14 Marina Road",
"line2": null,
"city": "Lagos",
"state": "Lagos",
"postalCode": "100001",
"country": "NG"
},
"tier": 1,
"hasBvn": true,
"hasNin": false,
"hasPassport": false,
"hasDriversLicence": false,
"hasTaxId": false,
"hasRegistrationNumber": false,
"createdAt": "2026-08-14T09:00:00.000Z",
"updatedAt": "2026-08-14T09:00:00.000Z"
}{
"statusCode": 400,
"message": "Must be a valid identifier.",
"code": "validation_failed",
"errors": [
{
"message": "Must be a valid identifier.",
"field": "accountId",
"code": "custom",
"path": [
"accountId"
]
}
]
}{
"statusCode": 401,
"code": "INVALID_API_KEY",
"message": "Invalid or inactive API credentials."
}{
"statusCode": 404,
"code": "resource_not_found",
"message": "Resource not found."
}{
"statusCode": 409,
"code": "resource_conflict",
"message": "The request conflicts with the current resource state."
}{
"statusCode": 429,
"message": "Too many API requests. Try again later.",
"code": "api_key_rate_limit_exceeded",
"retryAfterSeconds": 17
}{
"statusCode": 500,
"code": "internal_server_error",
"message": "An unexpected error occurred."
}Create a customer
Creates an individual or business customer. type selects which fields apply and cannot be changed afterwards. Identity numbers are accepted but never returned: the response reports presence through the has* flags. Every customer is created at tier 1.
curl --request POST \
--url https://sandbox-api.sakawave.example/v1/customers \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "individual",
"jurisdiction": "NG",
"externalReference": "cust_1001",
"email": "ada@example.com",
"phoneNumber": "+2348012345678",
"address": {
"line1": "14 Marina Road",
"line2": null,
"city": "Lagos",
"state": "Lagos",
"postalCode": "100001",
"country": "NG"
},
"firstName": "Ada",
"lastName": "Okafor",
"bvn": "22212345678"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'individual',
jurisdiction: 'NG',
externalReference: 'cust_1001',
email: 'ada@example.com',
phoneNumber: '+2348012345678',
address: {
line1: '14 Marina Road',
line2: null,
city: 'Lagos',
state: 'Lagos',
postalCode: '100001',
country: 'NG'
},
firstName: 'Ada',
lastName: 'Okafor',
bvn: '22212345678'
})
};
fetch('https://sandbox-api.sakawave.example/v1/customers', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://sandbox-api.sakawave.example/v1/customers"
payload = {
"type": "individual",
"jurisdiction": "NG",
"externalReference": "cust_1001",
"email": "ada@example.com",
"phoneNumber": "+2348012345678",
"address": {
"line1": "14 Marina Road",
"line2": None,
"city": "Lagos",
"state": "Lagos",
"postalCode": "100001",
"country": "NG"
},
"firstName": "Ada",
"lastName": "Okafor",
"bvn": "22212345678"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "01K2Z3AB4C5D6E7F8G9HJKMNPQ",
"type": "individual",
"jurisdiction": "NG",
"externalReference": "cust_1001",
"displayName": "Ada Okafor",
"name": null,
"firstName": "Ada",
"middleName": null,
"lastName": "Okafor",
"email": "ada@example.com",
"phoneNumber": "+2348012345678",
"dateOfBirth": "1992-04-18",
"gender": "female",
"address": {
"line1": "14 Marina Road",
"line2": null,
"city": "Lagos",
"state": "Lagos",
"postalCode": "100001",
"country": "NG"
},
"tier": 1,
"hasBvn": true,
"hasNin": false,
"hasPassport": false,
"hasDriversLicence": false,
"hasTaxId": false,
"hasRegistrationNumber": false,
"createdAt": "2026-08-14T09:00:00.000Z",
"updatedAt": "2026-08-14T09:00:00.000Z"
}{
"statusCode": 400,
"message": "Must be a valid identifier.",
"code": "validation_failed",
"errors": [
{
"message": "Must be a valid identifier.",
"field": "accountId",
"code": "custom",
"path": [
"accountId"
]
}
]
}{
"statusCode": 401,
"code": "INVALID_API_KEY",
"message": "Invalid or inactive API credentials."
}{
"statusCode": 404,
"code": "resource_not_found",
"message": "Resource not found."
}{
"statusCode": 409,
"code": "resource_conflict",
"message": "The request conflicts with the current resource state."
}{
"statusCode": 429,
"message": "Too many API requests. Try again later.",
"code": "api_key_rate_limit_exceeded",
"retryAfterSeconds": 17
}{
"statusCode": 500,
"code": "internal_server_error",
"message": "An unexpected error occurred."
}Normalization
Values are normalized before they are stored, and the response shows the stored form: text is trimmed,email is lowercased, and jurisdiction and address.country are uppercased. Compare against the response, not against what you sent.
Uniqueness
Within your business,externalReference and each identity number must be unique among customers that are not archived. Archiving frees the value for reuse.
customer.created webhook carrying this same response body.Error codes
| Status | code | Cause |
|---|---|---|
400 | validation_failed | A field failed validation, or email uses a disposable domain — that failure carries disposable_email_not_allowed in errors[].code. |
409 | customer_already_exists | The externalReference or an identity number is already on file for an active customer of yours. |
401 | INVALID_API_KEY | The key is missing, invalid, inactive, expired, from the other environment, or blocked by its IP allowlist. |
404 | client_not_found | The key authenticated, but its business no longer exists. |
409 | client_not_a_business | Customer management is available only to business clients. |
429 | api_key_rate_limit_exceeded | The per-minute limit for this key was exceeded. |
Authorizations
A secret API key, sent as Authorization: Bearer sk_test_… in sandbox or Bearer sk_live_… in live.
Body
- Option 1
- Option 2
21 - 120255^\+[1-9]\d{6,19}$Show child attributes
Show child attributes
1 - 1001 - 100100male, female, other ^\d{11}$^\d{11}$505050Response
Successful response.
A 26-character ULID.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
Fixed at creation. It decides which of the fields below apply.
individual, business Uppercase ISO 3166-1 alpha-2 country of the relationship. Fixed at creation.
2Your own reference. Unique among your customers that are not archived.
The business name, or the personal names joined, or "Unnamed customer" when neither is on file. Always safe to render.
male, female, other Show child attributes
Show child attributes
Verification tier. Every customer is created at tier 1. It becomes 3 when the customer passes verification, announced by customer.kyc.approved. No endpoint sets it directly, and a later suspension does not lower it.
1, 2, 3 Whether a BVN is on file. The number itself is never returned.
Whether a NIN is on file.
Whether a passport number is on file.
Whether a driving licence number is on file.
Whether a tax identifier is on file.
Whether a company registration number is on file. Business customers only.
"2026-08-14T09:00:00.000Z"
"2026-08-14T09:00:00.000Z"