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

# Collect mobile money payments

> Initiate mobile money collections, track their status and understand settlement for your workspace merchant account.

Before you can collect payments, your workspace merchant account must be activated in the MasterID app. The Merchant ID shown in your Developer Console is from your own workspace, and only your own workspace credentials can initiate collections for that Merchant ID.

## Prerequisites

1. Complete merchant activation in the MasterID app.
2. Generate API credentials from the Developer Console and note the Merchant ID.
3. Ensure the payer mobile number is supported for Ugandan MTN or Airtel mobile money.

## Collection constraints

| Field | Constraint |
| - | - |
| `merchantId` | Must match the Merchant ID of the authenticated workspace |
| `amount` | Integer UGX, minimum 10,000, maximum 20,000,000 |
| `reason` | 2 to 30 characters |
| `payerName` | 2 to 120 characters |
| `payerAddress` | 2 to 160 characters |
| `payerPhone` | Must be a supported Uganda mobile money number |

## Initiate a collection

Send a `POST /collections` request with an `Idempotency-Key` header. The request runs against your authenticated workspace merchant account. If the Merchant ID does not match your workspace, the request is rejected with `403`.

```bash theme={null}
POST /collections
Idempotency-Key: <UNIQUE_REQUEST_ID>
{
  "merchantId": "<YOUR_MERCHANT_ID>",
  "payerName": "<PAYER_NAME>",
  "payerPhone": "<UGANDA_MOBILE_NUMBER>",
  "payerAddress": "<PAYER_ADDRESS>",
  "amount": 10000,
  "reason": "Invoice payment"
}
```

Example response:

```json theme={null}
{
  "id": "<REQUEST_ID>",
  "data": {
    "paymentCode": "<PAYMENT_CODE>",
    "reference": "<COLLECTION_REFERENCE>",
    "status": "pending"
  }
}
```

> [!WARNING]
>
> Never report a successful payment from the initiation response. The `pending` status only confirms that the request has been accepted and sent to the payer. The payer must authorise the request on their phone before the payment completes.

## Track collection status

Use the `reference` from the creation response to poll or correlate webhooks.

| Status | Meaning |
| - | - |
| `pending` | Awaiting payer authorisation |
| `success` | Payer authorised and funds are captured |
| `failed` | Payer did not authorise or the request was rejected |

Poll: `GET /collections/{reference}`.

Event: `collection.completed` is delivered to your configured webhook URL.

Read responses include `amount_ugx` and `completed_at` for completed collections.

## Settlement and withdrawals

Funds from successful collections become available on the next working day. Manage withdrawals in the MasterID app.

## Collection-specific rate limits

MasterID enforces the following limits in addition to the general API rate limit:

| Scope | Limit |
| - | - |
| Per workspace per hour | 40 requests |
| Per workspace per day | 200 requests |
| Per phone per 15 minutes | 3 requests |
| Per phone per workspace per day | 8 requests |
| Per phone across MasterID per day | 10 requests |

If you receive a `429` response, wait for the value in the `Retry-After` header before retrying.

## End-to-end examples

The examples include constraint validation before submission, then polling for the final status.

```javascript Node.js (18+, native fetch, crypto) theme={null}
const crypto = require('node:crypto');

const API_KEY = process.env.MASTERID_API_KEY;
const API_SECRET = process.env.MASTERID_API_SECRET;
const MERCHANT_ID = process.env.MASTERID_MERCHANT_ID;
const BASE = 'https://masterid.me/api/public/v1';

function validate(input) {
  if (input.amount < 10000 || input.amount > 20000000 || !Number.isInteger(input.amount)) {
    throw new Error('amount must be an integer between 10,000 and 20,000,000 UGX');
  }
  if (!input.reason || input.reason.length < 2 || input.reason.length > 30) {
    throw new Error('reason must be 2-30 characters');
  }
  if (!input.payerName || input.payerName.length < 2 || input.payerName.length > 120) {
    throw new Error('payerName must be 2-120 characters');
  }
  if (!input.payerAddress || input.payerAddress.length < 2 || input.payerAddress.length > 160) {
    throw new Error('payerAddress must be 2-160 characters');
  }
  if (!input.payerPhone) {
    throw new Error('payerPhone is required');
  }
  if (input.merchantId !== MERCHANT_ID) {
    throw new Error('merchantId must match your workspace Merchant ID');
  }
}

async function masterid(path, opts = {}) {
  const res = await fetch(`${BASE}${path}`, {
    headers: {
      'X-API-Key': API_KEY,
      'X-API-Secret': API_SECRET,
      'Content-Type': 'application/json',
      ...(opts.headers || {}),
    },
    ...opts,
  });
  const body = await res.json().catch(() => null);
  return { status: res.status, body };
}

async function collectPayment() {
  const input = {
    merchantId: MERCHANT_ID,
    payerName: 'John Doe',
    payerPhone: '256712345678',
    payerAddress: 'Kampala, Uganda',
    amount: 15000,
    reason: 'Invoice payment',
  };

  validate(input);

  const idempotencyKey = crypto.randomBytes(16).toString('hex');
  const create = await masterid('/collections', {
    method: 'POST',
    headers: { 'Idempotency-Key': idempotencyKey },
    body: JSON.stringify(input),
  });

  const reference = create.body?.data?.reference;
  if (!reference) throw new Error('Collection reference missing');
  console.log('Collection reference:', reference, 'status:', create.body?.data?.status);

  if (create.status === 200 && create.body?.data?.status !== 'pending') {
    return create.body;
  }

  while (true) {
    await new Promise(r => setTimeout(r, 15000));
    const poll = await masterid(`/collections/${reference}`);
    const status = poll.body?.data?.status;
    console.log('Poll status:', status);
    if (status !== 'pending') {
      return poll.body;
    }
  }
}

collectPayment().catch(console.error);
```

```python Python (requests) theme={null}
import os, time, re, uuid
import requests

API_KEY = os.environ["MASTERID_API_KEY"]
API_SECRET = os.environ["MASTERID_API_SECRET"]
MERCHANT_ID = os.environ["MASTERID_MERCHANT_ID"]
BASE = "https://masterid.me/api/public/v1"

HEADERS = {
    "X-API-Key": API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
}

def validate(data):
    amount = data.get("amount")
    if not isinstance(amount, int) or amount < 10000 or amount > 20000000:
        raise ValueError("amount must be an integer between 10,000 and 20,000,000 UGX")
    name = data.get("reason") or ""
    if len(name) < 2 or len(name) > 30:
        raise ValueError("reason must be 2-30 characters")
    pname = data.get("payerName") or ""
    if len(pname) < 2 or len(pname) > 120:
        raise ValueError("payerName must be 2-120 characters")
    addr = data.get("payerAddress") or ""
    if len(addr) < 2 or len(addr) > 160:
        raise ValueError("payerAddress must be 2-160 characters")
    if not data.get("payerPhone"):
        raise ValueError("payerPhone is required")
    if data.get("merchantId") != MERCHANT_ID:
        raise ValueError("merchantId must match your workspace Merchant ID")

def masterid(method, path, extra_headers=None, json_body=None):
    h = {**HEADERS, **(extra_headers or {})}
    resp = requests.request(method, f"{BASE}{path}", headers=h, json=json_body, timeout=30)
    return resp.status_code, resp.json() if resp.text else None

def collect_payment():
    payload = {
        "merchantId": MERCHANT_ID,
        "payerName": "John Doe",
        "payerPhone": "256712345678",
        "payerAddress": "Kampala, Uganda",
        "amount": 15000,
        "reason": "Invoice payment",
    }
    validate(payload)

    idempotency_key = str(uuid.uuid4())
    status, create = masterid("POST", "/collections", extra_headers={"Idempotency-Key": idempotency_key}, json_body=payload)
    reference = create["data"]["reference"] if create and create.get("data") else ""
    if not reference:
        raise ValueError("Collection reference missing")

    if status == 200 and create["data"].get("status") != "pending":
        return create

    while True:
        time.sleep(15)
        status, poll = masterid("GET", f"/collections/{reference}")
        status_value = poll["data"]["status"] if poll and poll.get("data") else None
        print("Poll status:", status_value)
        if status_value != "pending":
            return poll

if __name__ == "__main__":
    print(collect_payment())
```

## Sequence diagram

```mermaid theme={null}
sequenceDiagram
  participant Payer as Payer Phone
  participant Server as Your Backend
  participant MasterID as MasterID API
  Server->>MasterID: POST /collections + Idempotency-Key
  MasterID-->>Server: pending + reference
  MasterID->>Payer: Mobile money push request
  Payer->>Payer: Authorises on device
  alt Polling
    loop Every >=15s
      Server->>MasterID: GET /collections/{reference}
      MasterID-->>Server: pending / success / failed
    end
  end
  alt Webhook
    MasterID->>Server: POST collection.completed
    Server-->>MasterID: 2xx
  end
```


## Related topics

- [Receive webhooks](https://docs.masterid.co.ug/guides/webhooks.md)
- [POST /collections](https://docs.masterid.co.ug/api-reference/collections/create-collection.md)
- [MasterID Integration API](https://docs.masterid.co.ug/index.md)
- [API Overview](https://docs.masterid.co.ug/api-reference/overview.md)
- [GET /collections/{reference}](https://docs.masterid.co.ug/api-reference/collections/get-collection.md)


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