> ## 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.

# Investing for a Customer

> Start an investment in a plan for one of your customers, by bank transfer or M-Pesa.

An investment puts a customer's money into one plan. You start it with `invest`, which creates a **Payment**: one attempt to move money to Ndovu, with its own **reference**. Once the money arrives, the Payment completes and a **Transaction** is created for the investment. Ndovu then invests it, and the customer receives units in the plan.

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

Whether this is the customer's first investment in the plan or a top-up is decided by us from the customer's holdings. You never say which.

## Before you start

* Your account has a `referenceCode` in [your configuration](https://docs.ndovu.co/configuration.md). Your customers' payment references start with it (e.g. `ACM7F3K9Q`). Until it's set, `invest` returns `403 PARTNER_REFERENCE_CODE_MISSING`.

* The customer is [onboarded](https://docs.ndovu.co/onboarding-customers.md).

* The plan is one of [your products](https://docs.ndovu.co/getting-products.md) and accepts new money (`investable: true`).

* The channel is enabled for your account: `bankTransferEnabled` or `mpesaEnabled` in [your configuration](https://docs.ndovu.co/configuration.md#funding-channels).

## Request

**Bank transfer**

```bash theme={null}
curl -X POST https://be-dev-1.dev.ndovu.co/api/v3/partners/YOUR_SHORT_NAME/customers/cust_8842/invest \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: 3f9c1e2a-5b7d-4c1e-9f0a-8d2b6e4a1c33" \
  -H "Content-Type: application/json" \
  -d '{
    "planId": 12,
    "amount": 5000,
    "currency": "KES",
    "channel": "BANK_TRANSFER"
  }'
```

**M-Pesa**

```bash theme={null}
curl -X POST https://be-dev-1.dev.ndovu.co/api/v3/partners/YOUR_SHORT_NAME/customers/cust_8842/invest \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: 3f9c1e2a-5b7d-4c1e-9f0a-8d2b6e4a1c33" \
  -H "Content-Type: application/json" \
  -d '{
    "planId": 12,
    "amount": 5000,
    "currency": "KES",
    "channel": "MPESA",
    "phoneNumber": "+254712345678"
  }'
```

**Node.js**

```javascript theme={null}
const res = await fetch(
  `https://be-dev-1.dev.ndovu.co/api/v3/partners/${shortName}/customers/${externalUserId}/invest`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      // One key per intended investment, saved with your own order record.
      "Idempotency-Key": order.idempotencyKey,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      planId: 12,
      amount: 5000,
      currency: "KES",
      channel: "BANK_TRANSFER",
    }),
  }
);

const { data: payment } = await res.json();
```

**Python**

```python theme={null}
res = requests.post(
    f"https://be-dev-1.dev.ndovu.co/api/v3/partners/{short_name}/customers/{external_user_id}/invest",
    headers={
        "Authorization": f"Bearer {access_token}",
        # One key per intended investment, saved with your own order record.
        "Idempotency-Key": order.idempotency_key,
    },
    json={
        "planId": 12,
        "amount": 5000,
        "currency": "KES",
        "channel": "BANK_TRANSFER",
    },
)
res.raise_for_status()
payment = res.json()["data"]
```

### Idempotency-Key

Every `invest` call needs an `Idempotency-Key` header: a value you generate (a UUID is a good choice) that identifies **one intended investment** on your side. Save it with your own order record **before** calling us, and send the same key on every retry of that investment. See [Retrying](#retrying).

### Body

| Field | Type | Required | Description |
| - | - | - | - |
| `planId` | integer | Yes | A plan `id` from [Getting Products](https://docs.ndovu.co/getting-products.md). |
| `amount` | number | Yes | How much to invest, in `currency`. At least the plan's minimum for that currency (see below). |
| `currency` | string | Yes | `KES`, `USD`, `RWF`, `TZS`, `EUR` or `GBP`. The plan must have a minimum set for it. |
| `channel` | string | Yes | `BANK_TRANSFER` or `MPESA`. |
| `phoneNumber` | string | `MPESA` only | The number that receives the M-Pesa prompt, e.g. `+254712345678`. Any valid number: it doesn't have to be the customer's own. |

Any other field is refused.

### Minimums

Every plan has a minimum per currency, and a separate minimum for top-ups. They are in the plan's `minInvestableAmounts` (first investment) and `minTopUpAmounts` (top-up) from [Getting Products](https://docs.ndovu.co/getting-products.md). The same minimums apply to every channel and every partner. A currency with no minimum set can't be used for that plan.

## Response

`201 Created` with the new Payment (or `200 OK` with the same Payment when you resend a key you've used before):

```json theme={null}
{
  "message": "Investment initiated successfully",
  "data": {
    "reference": "ACM7F3K9Q",
    "channel": "BANK_TRANSFER",
    "amount": 5000,
    "finalAmount": 5000,
    "currency": "KES",
    "status": "PENDING",
    "mnoReference": null,
    "bankReference": null,
    "phoneNumber": null,
    "plan": { "id": 12, "name": "Ndovu Money Market Fund" },
    "transactionId": null,
    "createdAt": "2026-10-23T08:00:00.000Z",
    "completedAt": null,
    "bankDetails": {
      "bankName": "Example Bank",
      "bankBranch": "Example Branch",
      "accountName": "Ndovu Example Account",
      "accountNumber": "0000000000",
      "swiftCode": "EXAMPLEXXX"
    },
    "fee": 50
  }
}
```

> [!WARNING]
>
> The bank details above are example values, not a real account. Always show
> your customer the `bankDetails` from the response, never hard-coded account
> details: they can differ by currency and environment.

The fields are described in [Payments](https://docs.ndovu.co/payments.md#the-payment-object). Two are only on this response:

| Field | Description |
| - | - |
| `fee` | The deposit fee this investment will be charged. It is taken from the amount before investing. |
| `bankDetails` | Where to send a bank transfer (see below). `null` for M-Pesa. |

`transactionId` is `null` until the Payment completes. That's when the Transaction is created.

## Completing the payment

**Bank transfer**

Show your customer the transfer instructions, built from the response:

> Send `currency` `finalAmount` to `bankDetails.accountName`, `bankDetails.bankName`, account `bankDetails.accountNumber`, with the reference `reference`.

With the example response above, that reads: send **KES 5,000** to **Ndovu Example Account**, Example Bank, account **0000000000**, with the reference **ACM7F3K9Q**.

The customer (or you, on their behalf) makes the transfer. **The reference must be in the transfer's memo or narration**: it's how we match the money to the Payment. The amount must be exactly `finalAmount`.

A bank transfer that never arrives expires after 5 days and the Payment becomes `FAILED`. If the money arrives later, we still match it and our team follows up.

**M-Pesa**

An M-Pesa prompt goes to `phoneNumber` straight away. The customer enters their PIN on their phone. When M-Pesa confirms, the Payment completes; if the customer cancels or doesn't answer, it becomes `FAILED`.

If Safaricom rejects the prompt before it reaches the phone, `invest` returns an error and the Payment is `FAILED`.

Then follow the Payment and the Transaction as described in [Checking Status](https://docs.ndovu.co/checking-status.md).

## Retrying

If a call times out or fails with a `5xx`, **resend it with the same `Idempotency-Key`**. You can never create the same investment twice:

| What you send | What you get |
| - | - |
| A new key | A new Payment (`201`) |
| The same key and body again | The Payment that key created (`200`), whatever its status now. No second M-Pesa prompt or bank reference |
| The same key with a different customer, plan, amount, currency, channel or phone | `409 IDEMPOTENCY_KEY_REUSED`. Nothing is created |
| No key | `400 IDEMPOTENCY_KEY_REQUIRED` |

Keys never expire. To try again after a Payment `FAILED`, call `invest` with a **new** key: a failed Payment is never reused, and resending its key just returns it, still `FAILED`.

## Errors

| Status | Code | Cause |
| - | - | - |
| `400` | `IDEMPOTENCY_KEY_REQUIRED` | No `Idempotency-Key` header, or one over 255 characters. |
| `400` | `BAD_REQUEST` | A field is missing, malformed or not allowed, or `phoneNumber` is missing for `MPESA`. |
| `400` | | The amount is below the plan's minimum for the currency, or no minimum is set for that currency. The `message` gives the minimum. |
| `400` | `MAXIMUM_TRANSFER_LIMIT_EXCEEDED` | The amount is over the channel's limit for one payment. |
| `400` | `DEPOSIT_WITH_PENDING_WITHDRAW_TRANSACTION` | The customer has a withdrawal pending on this plan. |
| `403` | `PARTNER_REFERENCE_CODE_MISSING` | Your account has no reference code yet. Contact us. |
| `403` | `FUNDING_METHOD_NOT_ENABLED` | The channel isn't enabled for your account. |
| `403` | `PARTNER_DISABLED` | Your account is disabled. |
| `404` | `CUSTOMER_NOT_FOUND` | You have no customer with this `externalUserId`. |
| `404` | `PLAN_NOT_FOUND` | The plan isn't one of your products. |
| `409` | `IDEMPOTENCY_KEY_REUSED` | This key was already used for a different investment. |
| `422` | `PLAN_NOT_INVESTABLE` | The plan isn't accepting new money right now. `message` says why. |

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


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