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

# Onboarding a Customer

> Create your customers as Ndovu investors, read them back, and keep their details up to date.

Onboarding creates your customer as a Ndovu investor, ready to invest. You identify each customer by **your own ID for them**, `externalUserId`; you never need to store an ID of ours.

What you send depends on your [`identityPolicy`](https://docs.ndovu.co/configuration#identitypolicy): with `SHARED` you send the customer's identity, with `REFERENCED` you send none.

## Create a customer

```
POST /api/v3/partners/{shortName}/customers
```

**SHARED**

```bash theme={null}
curl -X POST https://be-dev-1.dev.ndovu.co/api/v3/partners/YOUR_SHORT_NAME/customers \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "externalUserId": "cust_8842",
    "firstName": "Jane",
    "lastName": "Wanjiku",
    "email": "jane.wanjiku@example.com",
    "phoneNumber": "+254712345678",
    "termsAndConditionsAccepted": true,
    "kycVerification": {
      "verifiedOn": "2026-10-14",
      "reference": "KYC-2026-00931"
    },
    "kyc": {
      "identificationType": "NATIONAL_ID",
      "identificationNumber": "29384756",
      "identificationCountry": "KE",
      "residencyCountry": "KE",
      "dateOfBirth": "1991-04-02",
      "gender": "FEMALE",
      "taxPayerNumber": "A012345678Z",
      "city": "Nairobi",
      "sourceOfFunds": "SALARY_OR_BUSINESS_PROFIT",
      "employmentStatus": "Full Time"
    }
  }'
```

**REFERENCED**

```bash theme={null}
curl -X POST https://be-dev-1.dev.ndovu.co/api/v3/partners/YOUR_SHORT_NAME/customers \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "externalUserId": "938291",
    "termsAndConditionsAccepted": true,
    "kycVerification": {
      "verifiedOn": "2026-10-14",
      "reference": "KYC-2026-00931"
    }
  }'
```

Send nothing else. Any name, contact detail or `kyc` field is refused with `400 CUSTOMER_PII_NOT_ACCEPTED`, so you know none of it was stored.

### Body

| Field | Type | Required | Description |
| - | - | - | - |
| `externalUserId` | string | Yes | Your own ID for the customer, up to 255 characters. Unique within your account. |
| `firstName` | string | `SHARED` only | Not accepted for `REFERENCED`. |
| `lastName` | string | `SHARED` only | Not accepted for `REFERENCED`. |
| `email` | string | `SHARED` only | A valid email. Stored in lowercase. Not accepted for `REFERENCED`. |
| `phoneNumber` | string | `SHARED` only | In international format, e.g. `+254712345678`. Not accepted for `REFERENCED`. |
| `termsAndConditionsAccepted` | boolean | Yes | Must be `true`: the customer accepted Ndovu's terms and conditions. |
| `kycVerification.verifiedOn` | string | Yes | The date you completed KYC on this customer, as `YYYY-MM-DD`. |
| `kycVerification.reference` | string | No | Your own KYC case or reference ID, up to 255 characters. |
| `kyc` | object | No | The customer's KYC details, all optional. `SHARED` only. |

### `kyc` fields

All optional. Send what you hold; anything you leave out is returned as `null`.

| Field | Type | Values |
| - | - | - |
| `identificationType` | string | `NATIONAL_ID`, `PASSPORT`, `ALIEN_ID`, `ELECTRONIC_ID_CARD`, `ID_BOOK`, `RESIDENT_ID`, `SEAMAN_ID`, `SOCIAL_ID`, `KE_ARMY_ID`, `DRIVING_LICENSE`, `TRAVEL_DOCS`, `VOTING_CARD` |
| `identificationNumber` | string | The ID number. Unique within your account for the same `identificationCountry`. |
| `identificationCountry` | string | Uppercase ISO 3166-1 alpha-2 code of the issuing country, e.g. `KE`. |
| `residencyCountry` | string | Uppercase ISO 3166-1 alpha-2 code of the country the customer lives in. |
| `dateOfBirth` | string | `YYYY-MM-DD`. The customer must be at least 18. |
| `gender` | string | `MALE`, `FEMALE`, `NON_BINARY` |
| `taxPayerNumber` | string | Tax identification number. For Kenyan customers, the KRA PIN. |
| `residentialAddress` | string | |
| `city` | string | |
| `postalCode` | string | |
| `postalAddress` | string | |
| `sourceOfFunds` | string | `SALARY_OR_BUSINESS_PROFIT`, `BANK_CREDIT`, `PERSONAL_SAVINGS`, `PROPERTY_SALES`, `INHERITANCE`, `SHARE_SALES_OR_DIVIDEND` |
| `employmentStatus` | string | `Full Time`, `Part Time`, `Contractor`, `Not employed` |
| `nextOfKinNames` | string | |
| `nextOfKinContact` | string | A phone number in international format. |
| `nextOfKinEmail` | string | A valid email. |

Fields not listed here are refused, so a typo fails loudly instead of being dropped.

### Response

`201 Created` for a new customer:

```json theme={null}
{
  "message": "Customer created successfully",
  "data": {
    "externalUserId": "cust_8842",
    "investorId": "3f9c2a1e-7b4d-4c8e-9a61-2d5f0b8e4c17",
    "kycStatus": "COMPLETED",
    "country": "KE",
    "currency": "KES",
    "firstName": "Jane",
    "lastName": "Wanjiku",
    "email": "jane.wanjiku@example.com",
    "phoneNumber": "+254712345678",
    "kyc": {
      "identificationType": "NATIONAL_ID",
      "identificationNumber": "29384756",
      "identificationCountry": "KE",
      "residencyCountry": "KE",
      "dateOfBirth": "1991-04-02",
      "gender": "FEMALE",
      "taxPayerNumber": "A012345678Z",
      "residentialAddress": null,
      "city": "Nairobi",
      "postalCode": null,
      "postalAddress": null,
      "sourceOfFunds": "SALARY_OR_BUSINESS_PROFIT",
      "employmentStatus": "Full Time",
      "nextOfKinNames": null,
      "nextOfKinContact": null,
      "nextOfKinEmail": null
    },
    "documents": [],
    "createdAt": "2026-10-22T09:14:03.511Z",
    "updatedAt": "2026-10-22T09:14:03.511Z"
  }
}
```

| Field | Description |
| - | - |
| `externalUserId` | Your ID for the customer. |
| `investorId` | Ndovu's ID for the investor, a UUID. Returned for reference only; you never send it. |
| `kycStatus` | `COMPLETED` on creation, on the strength of your `kycVerification`. |
| `country` / `currency` | Taken from your [configuration](https://docs.ndovu.co/configuration.md). |
| `firstName`, `lastName`, `email`, `phoneNumber` | Always `null` for `REFERENCED`. |
| `kyc` | Every KYC field, `null` where not sent. |
| `documents` | Uploaded documents. See [Uploading Documents](https://docs.ndovu.co/uploading-documents.md). |
| `createdAt` / `updatedAt` | ISO 8601 timestamps. |

### Retrying safely

Create is idempotent on `externalUserId`, so you can retry after a timeout without creating a duplicate:

* Same `externalUserId` and same details: `200 OK` with `"message": "Customer already exists"` and the existing customer.
* Same `externalUserId` and different details: `409 CUSTOMER_EXTERNAL_ID_CONFLICT`. Use [update](#update-a-customer) to change details.

For `REFERENCED`, any resend of an existing `externalUserId` is a retry and returns `200`.

## List customers

```
GET /api/v3/partners/{shortName}/customers
```

Your customers, newest first.

```bash theme={null}
curl "https://be-dev-1.dev.ndovu.co/api/v3/partners/YOUR_SHORT_NAME/customers?createdFrom=2026-10-01&limit=20" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

| Parameter | Description |
| - | - |
| `kycStatus` | Only customers with this KYC status, e.g. `COMPLETED`. |
| `createdFrom` | Only customers onboarded on or after this date (`YYYY-MM-DD`). |
| `createdTo` | Only customers onboarded on or before this date (`YYYY-MM-DD`). |
| `limit` | Up to 50. Default 20. |
| `cursor` | The `nextCursor` from the previous page: the `externalUserId` of its last customer. |

```json theme={null}
{
  "message": "Request executed successfully",
  "data": {
    "data": [
      {
        "externalUserId": "cust_8842",
        "investorId": "3f9c2a1e-7b4d-4c8e-9a61-2d5f0b8e4c17",
        "kycStatus": "COMPLETED",
        "country": "KE",
        "currency": "KES",
        "firstName": "Jane",
        "lastName": "Wanjiku",
        "email": "jane.wanjiku@example.com",
        "phoneNumber": "+254712345678",
        "kyc": { "identificationType": "NATIONAL_ID", "identificationNumber": "29384756" },
        "createdAt": "2026-10-22T09:14:03.511Z",
        "updatedAt": "2026-10-22T09:14:03.511Z"
      }
    ],
    "nextCursor": null
  }
}
```

Each item is the customer as [Get a customer](#get-a-customer) returns it, without `documents` (their download links are created per read, so get the customer for them). With `REFERENCED`, the identity fields are `null`. `nextCursor` is the last customer's `externalUserId`, or `null` on the last page; pass it as `cursor` to get the next page. A `cursor` that isn't one of your customers returns `400 INVALID_CUSTOMER_CURSOR`.

## Get a customer

```
GET /api/v3/partners/{shortName}/customers/{externalUserId}
```

```bash theme={null}
curl https://be-dev-1.dev.ndovu.co/api/v3/partners/YOUR_SHORT_NAME/customers/cust_8842 \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Returns the same shape as create, with `200 OK`, or `404 CUSTOMER_NOT_FOUND` if you have no customer with that `externalUserId`. You can only see your own customers.

## Update a customer

```
PATCH /api/v3/partners/{shortName}/customers/{externalUserId}
```

Send only the fields you want to change. Any create field can be updated, identity included, except `externalUserId`, `termsAndConditionsAccepted` and `kycVerification`. Fields inside `kyc` are merged, so sending `kyc.city` leaves the other KYC fields as they were.

```bash theme={null}
curl -X PATCH https://be-dev-1.dev.ndovu.co/api/v3/partners/YOUR_SHORT_NAME/customers/cust_8842 \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane.w@example.com",
    "kyc": { "city": "Mombasa" }
  }'
```

An update is all or nothing: if any check fails, nothing is changed. Returns the updated customer with `200 OK`.

A `REFERENCED` account holds no customer details to update, so every update field is refused with `400 CUSTOMER_PII_NOT_ACCEPTED`.

## Uniqueness

Within your account:

* `externalUserId` identifies one customer.
* No two customers share an email or a phone number (`409 CUSTOMER_EMAIL_OR_PHONE_IN_USE`). `Jane@Example.com` and `jane@example.com` count as the same email.
* No two customers share an ID number for the same issuing country (`409 CUSTOMER_IDENTITY_IN_USE`).

These checks only look at your own customers. A person who is also a direct Ndovu customer, or a customer of another partner, can be onboarded by you.

## Errors

| Status | Code | Cause |
| - | - | - |
| `400` | `CUSTOMER_IDENTITY_REQUIRED` | `SHARED`: a required identity field is missing. `fields` lists which. |
| `400` | `CUSTOMER_PII_NOT_ACCEPTED` | `REFERENCED`: personal data was sent. `fields` lists which. |
| `400` | `BAD_REQUEST` | A field failed validation, or an unknown field was sent. `message` says which. |
| `401` | `INVALID_PARTNER_TOKEN` | Missing, expired or invalid access token. |
| `403` | `FORBIDDEN` | `shortName` is not yours. |
| `403` | `PARTNER_DISABLED` | Your account is disabled; create and update are refused. |
| `404` | `CUSTOMER_NOT_FOUND` | No customer with this `externalUserId`. |
| `409` | `CUSTOMER_EXTERNAL_ID_CONFLICT` | This `externalUserId` exists with different details. |
| `409` | `CUSTOMER_EMAIL_OR_PHONE_IN_USE` | Another of your customers has this email or phone. |
| `409` | `CUSTOMER_IDENTITY_IN_USE` | Another of your customers has this ID number. |

See [Errors](https://docs.ndovu.co/errors.md) for the error format.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.