> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.senjaropay.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.senjaropay.com/_mcp/server.

# Mobile money

Collect money from customers on mobile money. SenjaroPay sends a USSD push to the customer's phone; they authorize with their PIN.

Create the payment, then confirm the result with a [webhook](/webhooks). If the callback does not arrive, read the payment with [payment status](/payment-status).

All calls are **POST** to `https://api.senjaropay.com` and require **`Content-Type: application/json`**, **`x-api-key`**, and **`x-api-secret`**.

## How it works

#### Create a payment

Call create with the amount, customer phone, and customer details. Include a unique `x-idempotency-key`. Store `data.reference`.

#### Customer authorizes

The customer receives a USSD prompt on their wallet and enters their PIN.

#### Confirm the result

Prefer a [webhook](/webhooks) for the outcome. If the callback is late or missing, use [payment status](/payment-status) with the payment reference.

## Authentication

| Header              | Required | Description                                                  |
| ------------------- | -------- | ------------------------------------------------------------ |
| `Content-Type`      | Yes      | `application/json`                                           |
| `x-api-key`         | Yes      | Dashboard **Public Key** ([Authentication](/authentication)) |
| `x-api-secret`      | Yes      | Secret key. Server-side only.                                |
| `x-idempotency-key` | Yes      | A new UUID for each create request                           |

Generate the idempotency key in your backend. Reuse it only when retrying the **same** create request.

```js
import { randomUUID } from "node:crypto";

const idempotencyKey = randomUUID();
```

```python
import uuid

idempotency_key = str(uuid.uuid4())
```

Use **HTTPS** from your **backend only** — never send keys from a browser or mobile app.

## Supported networks

| Network      | Country |
| ------------ | ------- |
| Airtel Money | TZ      |
| M-Pesa       | TZ      |
| Mixx by Yas  | TZ      |
| Halotel      | TZ      |

## Create a payment

```
POST https://api.senjaropay.com/senjaropay/merchant/payments/create
```

SenjaroPay creates a pending collection and pushes USSD to `phone_number`. The session expires at `data.expires_at` if the customer does not complete payment. Do not fulfill the order while `data.status` is `pending`.

### Request

```json
{
  "payment_type": "mobile",
  "details": {
    "amount": 1000,
    "currency": "TZS"
  },
  "phone_number": "255700000001",
  "customer": {
    "firstname": "Test",
    "lastname": "Customer",
    "email": "test.customer@example.com",
    "country": "Tanzania",
    "city": "Dar es Salaam",
    "address": "Sample Street"
  },
  "submerchant_id": "",
  "webhook_url": "https://example.com/webhooks/senjaropay/mock-callback"
}
```

```bash
curl -sS -X POST "https://api.senjaropay.com/senjaropay/merchant/payments/create" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${SENJARO_API_KEY}" \
  -H "x-api-secret: ${SENJARO_API_SECRET}" \
  -H "x-idempotency-key: $(uuidgen)" \
  -d '{
    "payment_type": "mobile",
    "details": { "amount": 1000, "currency": "TZS" },
    "phone_number": "255700000001",
    "customer": {
      "firstname": "Test",
      "lastname": "Customer",
      "email": "test.customer@example.com",
      "country": "Tanzania",
      "city": "Dar es Salaam",
      "address": "Sample Street"
    },
    "submerchant_id": "",
    "webhook_url": "https://example.com/webhooks/senjaropay/mock-callback"
  }'
```

### Response

```json
{
  "status": "success",
  "code": 200,
  "data": {
    "amount": {
      "currency": "TZS",
      "value": 1000
    },
    "api_version": "2026-01-25",
    "expires_at": "2026-04-17T09:13:54.946Z",
    "object": "payment",
    "payment_type": "mobile",
    "reference": "mock-ref-7b2a4f3e",
    "status": "pending"
  }
}
```

### Request fields

| Field                | Type    | Required | Description                                                                                             |
| -------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `payment_type`       | string  | Yes      | Must be `mobile`                                                                                        |
| `details.amount`     | integer | Yes      | Amount to collect, in major units                                                                       |
| `details.currency`   | string  | Yes      | `TZS`                                                                                                   |
| `phone_number`       | string  | Yes      | Customer MSISDN, digits only (example `255700000001`)                                                   |
| `customer.firstname` | string  | Yes      | Customer first name                                                                                     |
| `customer.lastname`  | string  | Yes      | Customer last name                                                                                      |
| `customer.email`     | string  | No       | Customer email                                                                                          |
| `customer.country`   | string  | No       | Customer country                                                                                        |
| `customer.city`      | string  | No       | Customer city                                                                                           |
| `customer.address`   | string  | No       | Customer address                                                                                        |
| `submerchant_id`     | string  | No       | Sub-merchant ID from the dashboard. See [Introduction](/welcome#sub-merchant-id). Send `""` when unused |
| `webhook_url`        | string  | No       | HTTPS URL for payment result notifications                                                              |

## Errors

| HTTP status | Typical `error_code` | When                                               |
| ----------- | -------------------- | -------------------------------------------------- |
| **400**     | `validation_error`   | Missing field, invalid amount, or malformed JSON   |
| **401**     | `unauthorized`       | Missing or invalid `x-api-key` or `x-api-secret`   |
| **403**     | `insufficient_scope` | Key is valid but not allowed to create collections |

```json
{
  "status": "error",
  "code": 400,
  "error_code": "validation_error",
  "message": "amount is required"
}
```

If the same `x-idempotency-key` is reused, the API returns the previous transaction result instead of creating a new collection.

Examples on this page are mock values. Keep API keys in server-side secrets.