curl --request PATCH \
--url https://sandbox-api.sakawave.example/v1/customers/{customerId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"email": "ada.updated@example.com"
}
'const options = {
method: 'PATCH',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({email: 'ada.updated@example.com'})
};
fetch('https://sandbox-api.sakawave.example/v1/customers/{customerId}', 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/{customerId}"
payload = { "email": "ada.updated@example.com" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.patch(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.updated@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."
}Update a customer
Updates one or more mutable fields. Send only what changes; at least one field is required. type and jurisdiction are fixed at creation. Sending a field that belongs to the other customer type is refused rather than ignored.
curl --request PATCH \
--url https://sandbox-api.sakawave.example/v1/customers/{customerId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"email": "ada.updated@example.com"
}
'const options = {
method: 'PATCH',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({email: 'ada.updated@example.com'})
};
fetch('https://sandbox-api.sakawave.example/v1/customers/{customerId}', 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/{customerId}"
payload = { "email": "ada.updated@example.com" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.patch(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.updated@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."
}Null and absent are different
An absent field is left as it is. An explicitnull clears the stored value. "middleName": null erases a middle name; omitting middleName keeps it.
For an individual customer, displayName and name are recomposed from the first, middle, and last names whenever any of them changes.
customer.updated webhook carrying this same response body.Error codes
| Status | code | Cause |
|---|---|---|
400 | validation_failed | A field failed validation, or the body was empty. |
400 | customer_fields_do_not_match_type | A field for the other customer type was sent — name on an individual, or firstName on a business. The offending names are listed in fields. |
404 | customer_not_found | No such customer for this business, or it has been archived. |
409 | customer_already_exists | The new externalReference or identity number belongs to another 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.
Path Parameters
Customer ULID. A 26-character ULID.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
Body
1 - 120255^\+[1-9]\d{6,19}$Show child attributes
Show child attributes
1 - 1001001 - 100male, female, other 2 - 200^\d{11}$^\d{11}$50505050Response
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"