📘 API Reference v1

Build payments with PayIn API

Accept mobile money payments and send payouts across Tanzania. Integrate M-Pesa, Airtel Money, Tigo Pesa, and Halo Pesa with a single, unified API.

📥

Collect Payments

USSD push or invoice-based collection from any mobile money wallet.

📤

Send Payouts

Disburse funds to any phone number instantly. Batch support included.

🔔

Real-time Webhooks

Get notified instantly when payments complete with signed callbacks.

Authentication

All API requests require your API credentials sent as HTTP headers.

Authenticate every request by including these two headers:

HeaderDescription
X-API-KeyYour public API key from the dashboard
X-API-SecretYour secret API key (keep this private)
HeadersX-API-Key:    pk_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-API-Secret: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
⚠️
Keep your secret key safe. Never expose it in client-side code, public repositories, or frontend applications. Use environment variables on your server.

Generate your API keys from the PayIn Dashboard under Settings → API Keys. You can also configure IP whitelisting and webhook secrets there.

Base URL

All API endpoints are relative to this base URL.

Production
https://api.payin.co.tz/api/v1

All requests must be made over HTTPS. HTTP requests will be rejected.

Idempotency

Prevent duplicate transactions with idempotency keys.

For POST requests (collection, invoice, disbursement), include an X-Idempotency-Key header to ensure the request is processed only once — even if you retry due to a timeout or network error.

HeaderDescription
X-Idempotency-KeyA unique string (max 255 chars) for this request. Use a UUID or your own reference.

How It Works

cURLcurl -X POST https://api.payin.co.tz/api/v1/collection \
  -H "X-API-Key: pk_live_xxxx" \
  -H "X-API-Secret: sk_live_xxxx" \
  -H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"phone": "255714123456", "amount": 5000, "operator": "mpesa"}'

If you send the same X-Idempotency-Key again within 24 hours, the API returns the original response with an X-Idempotent-Replayed: true header instead of creating a duplicate transaction.

💡
Keys expire after 24 hours. After that, the same key can be reused. We recommend using UUIDs (e.g. 550e8400-e29b-41d4-a716-446655440000) or your internal order IDs as idempotency keys.
⚠️
Strongly recommended for payments. Without idempotency keys, retrying a timed-out request could double-charge your customer.

API Endpoints

Complete reference for all merchant-facing API endpoints.

POST /v1/collection Push USSD Collection

Initiate a USSD push collection. The customer receives a prompt on their phone to confirm the payment. Once confirmed, funds are deposited into your PayIn wallet and a webhook is sent to your callback URL.

Request Body

ParameterTypeRequiredDescription
phonestringRequiredCustomer phone number (10–15 digits). E.g. 255714123456
amountnumberRequiredAmount to collect. Min: 100, Max: 10,000,000
operatorstringOptionalOperator code: mpesa, airtel, tigopesa, halopesa. Auto-detected from phone number if omitted.
referencestringOptionalYour internal reference. Max 100 characters.
descriptionstringOptionalPayment description. Max 255 characters.
currencystringOptionalCurrency code. Default: TZS
callback_urlstringOptionalOverride the account-level callback URL for this request.

Example Request

cURLcurl -X POST https://api.payin.co.tz/api/v1/collection \
  -H "X-API-Key: pk_live_xxxx" \
  -H "X-API-Secret: sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "255714123456",
    "amount": 5000,
    "operator": "mpesa",
    "reference": "INV-001",
    "description": "Monthly subscription"
  }'

Success Response

201 Created

JSON{
  "success": true,
  "message": "Collection request sent to operator. Waiting for customer confirmation.",
  "request_ref": "PAYABCDEF123456",
  "operator_ref": "12345678",
  "status": "processing",
  "phone": "255714123456",
  "amount": 5000,
  "operator": "M-Pesa"
}

Error Response

422 Validation failed or operator unavailable.

JSON{
  "message": "Operator not found or inactive.",
  "errors": { "operator": ["Invalid operator"] }
}
POST /v1/invoice Create Invoice (Manual C2B)

Create a payment invoice. The customer pays manually using the provided reference number through their mobile money provider. Ideal for e-commerce, invoicing, and point-of-sale.

Request Body

ParameterTypeRequiredDescription
amountnumberRequiredAmount to collect. Min: 100, Max: 10,000,000
referencestringRequiredCustomer-facing payment reference. Max 100 chars.
descriptionstringOptionalInvoice description. Max 255 characters.
currencystringOptionalCurrency code. Default: TZS
phonestringOptionalCustomer phone for reference. 10–15 digits.
expires_inintegerOptionalExpiry in minutes. Min: 1, Max: 43200. Default: 10080 (7 days).

Example Request

cURLcurl -X POST https://api.payin.co.tz/api/v1/invoice \
  -H "X-API-Key: pk_live_xxxx" \
  -H "X-API-Secret: sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 50000,
    "reference": "INV-2026-001",
    "description": "Annual subscription",
    "expires_in": 1440
  }'

Success Response

201 Created

JSON{
  "success": true,
  "message": "Invoice created. Customer should pay using reference: INV-2026-001",
  "request_ref": "INVXYZ123456",
  "external_ref": "INV-2026-001",
  "amount": 50000,
  "currency": "TZS",
  "status": "waiting",
  "expires_at": "2026-04-20T10:30:00Z",
  "pay_url": "https://payin.co.tz/pay/a3f2b1c9d8e7..."
}
💡
The pay_url is a hosted payment page you can share with your customer. They can select their preferred operator and pay directly.
POST /v1/disbursement Send Payout

Send money to a mobile money number. The amount is deducted from your disbursement wallet. If your account has maker-checker enabled, the payout will require approval before processing.

Request Body

ParameterTypeRequiredDescription
phonestringRequiredRecipient phone number. 10–15 digits.
amountnumberRequiredAmount to send. Min: 100, Max: 10,000,000
operatorstringOptionalOperator code. Auto-detected from phone if not provided.
referencestringOptionalYour internal reference. Max 100 characters.
descriptionstringOptionalPayout description. Max 255 characters.
currencystringOptionalCurrency code. Default: TZS
callback_urlstringOptionalOverride the account-level callback URL for this payout.

Example Request

cURLcurl -X POST https://api.payin.co.tz/api/v1/disbursement \
  -H "X-API-Key: pk_live_xxxx" \
  -H "X-API-Secret: sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "255714123456",
    "amount": 10000,
    "reference": "SALARY-001",
    "description": "March salary"
  }'

Success Response — Direct Send

201 Created

JSON{
  "success": true,
  "message": "Disbursement request sent to operator.",
  "request_ref": "PAY123ABC456",
  "status": "processing",
  "phone": "255714123456",
  "amount": 10000,
  "operator": "M-Pesa"
}

Success Response — Pending Approval

201 Created

JSON{
  "success": true,
  "message": "Payout request submitted for approval.",
  "request_ref": "PAY123ABC456",
  "status": "pending_approval",
  "requires_approval": true
}

Error — Insufficient Balance

422

JSON{
  "success": false,
  "message": "Insufficient wallet balance.",
  "required": 10500,
  "available": 8000
}
GET /v1/status/{request_ref} Payment Status

Check the current status of any payment request — collection, invoice, or disbursement.

Path Parameters

ParameterTypeDescription
request_refstringThe request_ref returned when the payment was created.

Success Response

200 OK

JSON{
  "request_ref": "PAYABCDEF123456",
  "external_ref": "INV-001",
  "operator_ref": "12345678",
  "type": "collection",
  "phone": "255714123456",
  "amount": 5000,
  "charges": {
    "platform": 250.00,
    "operator": 75.00
  },
  "currency": "TZS",
  "operator": "M-Pesa",
  "status": "completed",
  "error": null,
  "created_at": "2026-04-19T10:15:00Z",
  "updated_at": "2026-04-19T10:20:00Z"
}
GET /v1/balance Account Balance

Retrieve your current wallet balances for collections and disbursements.

Success Response

200 OK

JSON{
  "account_id": "12345",
  "overall_balance": 250000.50,
  "collection_balance": 150000.00,
  "collection_held": 0.00,
  "disbursement_balance": 100000.50,
  "disbursement_held": 5000.00,
  "currency": "TZS",
  "updated_at": "2026-04-19T14:30:00Z"
}
GET /v1/transactions List Transactions

Retrieve a paginated list of your transactions with optional filtering by date, status, type, and operator.

Query Parameters

ParameterTypeDefaultDescription
date_fromstringFilter from date (YYYY-MM-DD)
date_tostringFilter to date (YYYY-MM-DD)
statusstringcompleted, failed, processing, pending
typestringcollection, disbursement, manual_c2b
operatorstringFilter by operator code
per_pageinteger20Results per page (1–100)

Example Request

cURLcurl "https://api.payin.co.tz/api/v1/transactions?date_from=2026-04-01&status=completed&per_page=50" \
  -H "X-API-Key: pk_live_xxxx" \
  -H "X-API-Secret: sk_live_xxxx"

Success Response

200 OK

JSON{
  "data": [
    {
      "request_ref": "PAYABCDEF123456",
      "type": "collection",
      "phone": "255714123456",
      "amount": 5000,
      "platform_charge": 250,
      "currency": "TZS",
      "operator": "M-Pesa",
      "status": "completed",
      "created_at": "2026-04-19T10:15:00Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 5,
    "per_page": 50,
    "total": 243
  }
}
GET /v1/transaction/{ref} Transaction Lookup

Look up a single transaction by request_ref, external_ref, or receipt_number.

Success Response

200 OK

JSON{
  "request_ref": "PAYABCDEF123456",
  "external_ref": "INV-001",
  "operator_ref": "12345678",
  "receipt_number": "MPI1234567890",
  "type": "collection",
  "phone": "255714123456",
  "amount": 5000,
  "platform_charge": 250,
  "operator_charge": 75,
  "currency": "TZS",
  "operator": "M-Pesa",
  "status": "completed",
  "callback_status": "sent",
  "created_at": "2026-04-19T10:15:00Z"
}
GET /v1/webhooks/{ref} Webhook Delivery Status

Check the webhook delivery status and attempt history for a specific transaction.

Success Response

200 OK

JSON{
  "request_ref": "PAYABCDEF123456",
  "callback_status": "sent",
  "callback_url": "https://merchant.com/webhook",
  "total_attempts": 2,
  "webhooks": [
    {
      "attempt": 1,
      "http_status": 500,
      "status": "failed",
      "response_time_ms": 5000,
      "sent_at": "2026-04-19T10:20:00Z"
    },
    {
      "attempt": 2,
      "http_status": 200,
      "status": "success",
      "response_time_ms": 250,
      "sent_at": "2026-04-19T10:21:30Z"
    }
  ]
}
GET /v1/operators List Operators

Get the list of active mobile money operators available for collections and disbursements.

Success Response

200 OK

JSON{
  "operators": [
    {
      "name": "M-Pesa",
      "code": "mpesa",
      "currency": "TZS",
      "status": "active",
      "maintenance_mode": false
    },
    {
      "name": "Airtel Money",
      "code": "airtel",
      "currency": "TZS",
      "status": "active",
      "maintenance_mode": false
    },
    {
      "name": "Tigo Pesa",
      "code": "tigopesa",
      "currency": "TZS",
      "status": "active",
      "maintenance_mode": false
    },
    {
      "name": "Halo Pesa",
      "code": "halopesa",
      "currency": "TZS",
      "status": "active",
      "maintenance_mode": false
    }
  ]
}
POST /v1/settlement Settle to Bank Account

Withdraw funds from your collection wallet to a bank account. The settlement goes through admin approval before being processed. You receive webhook callbacks at each stage: settlement.created, settlement.approved / settlement.rejected, settlement.completed.

Request Body

ParameterTypeRequiredDescription
amountnumberRequiredAmount to settle. Minimum: 1,000.
operatorstringRequiredSource wallet operator (e.g. M-Pesa, Tigo Pesa).
bank_account_idintegerRequiredID of your saved bank account. Use GET /v1/bank-accounts to list them.
descriptionstringOptionalDescription / memo for this settlement.
callback_urlstringOptionalURL to receive webhook callbacks for this settlement at each stage (settlement.created, settlement.approved, settlement.rejected, settlement.completed). Overrides your account-level callback URL.

Example Request

cURLcurl -X POST https://api.payin.co.tz/api/v1/settlement \
  -H "X-API-Key: pk_live_xxxx" \
  -H "X-API-Secret: sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 500000,
    "operator": "M-Pesa",
    "bank_account_id": 1,
    "description": "Weekly settlement"
  }'

Success Response

201 Created

JSON{
  "success": true,
  "message": "Settlement request created.",
  "settlement": {
    "settlement_ref": "STL-ABCD1234EFGH",
    "amount": 500000,
    "settlement_type": "bank",
    "operator": "M-Pesa",
    "currency": "TZS",
    "status": "pending",
    "bank_name": "CRDB Bank",
    "account_number": "0150123456789",
    "account_name": "My Company Ltd"
  },
  "charges": {
    "platform_charge": "2500.00",
    "operator_charge": "0.00",
    "total_debited": "502500.00"
  },
  "available_balance": "1497500.00"
}
💡
Settlement status flow: pendingapprovedcompleted. If rejected, funds are refunded to your collection wallet. You'll receive webhooks at each step.
POST /v1/settlement/usdt Settle to USDT Wallet

Withdraw funds from your collection wallet to a USDT cryptocurrency wallet. Supports TRC20, ERC20, and BEP20 networks.

Request Body

ParameterTypeRequiredDescription
amountnumberRequiredAmount to settle (in local currency). Minimum: 1,000.
operatorstringRequiredSource wallet operator (e.g. M-Pesa).
wallet_addressstringRequiredUSDT wallet address.
wallet_networkstringRequiredNetwork: TRC20, ERC20, or BEP20.
descriptionstringOptionalDescription / memo.
callback_urlstringOptionalURL to receive status webhooks for this settlement.

Example Request

cURLcurl -X POST https://api.payin.co.tz/api/v1/settlement/usdt \
  -H "X-API-Key: pk_live_xxxx" \
  -H "X-API-Secret: sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1000000,
    "operator": "M-Pesa",
    "wallet_address": "TXqH5j8k2n...",
    "wallet_network": "TRC20"
  }'

Success Response

201 Created

JSON{
  "success": true,
  "message": "Settlement request created.",
  "settlement": {
    "settlement_ref": "STL-WXYZ9876QRST",
    "amount": 1000000,
    "settlement_type": "usdt",
    "operator": "M-Pesa",
    "currency": "TZS",
    "status": "pending",
    "wallet_address": "TXqH5j8k2n...",
    "wallet_network": "TRC20"
  },
  "charges": { "platform_charge": "5000.00", "total_debited": "1005000.00" }
}
GET /v1/settlements List Settlements

Retrieve all your settlement requests with pagination.

Query Parameters

ParameterTypeDefaultDescription
statusstringFilter by status: pending, approved, rejected, completed
searchstringSearch by ref, bank name, or account name
per_pageinteger20Results per page (max 100)
GET /v1/settlement/{ref} Settlement Details

Get details of a settlement by its reference (STL-XXXX).

POST /v1/transfer Transfer: Collection → Disbursement

Move funds from your collection wallet to your disbursement wallet. This allows you to fund payouts from collected payments. Requires admin approval.

Request Body

ParameterTypeRequiredDescription
amountnumberRequiredAmount to transfer.
operatorstringRequiredOperator wallet to transfer within (e.g. M-Pesa).
descriptionstringOptionalDescription for the transfer.
callback_urlstringOptionalURL to receive webhooks when transfer is approved or rejected.

Example Request

cURLcurl -X POST https://api.payin.co.tz/api/v1/transfer \
  -H "X-API-Key: pk_live_xxxx" \
  -H "X-API-Secret: sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 200000,
    "operator": "M-Pesa",
    "description": "Fund disbursement wallet for payroll"
  }'

Success Response

201 Created

JSON{
  "success": true,
  "message": "Transfer request submitted. Pending admin approval.",
  "transfer": {
    "reference": "TRF-ABCDEF123456",
    "operator": "M-Pesa",
    "amount": 200000,
    "status": "pending"
  }
}
💡
Use case: You collect payments from customers (collection wallet fills up). To send payouts, transfer funds to your disbursement wallet first, then call POST /v1/disbursement.
GET /v1/transfers List Transfers

List your internal transfer requests with pagination.

GET /v1/bank-accounts List Bank Accounts

List your saved bank accounts. Use the returned id when creating bank settlements.

Success Response

JSON{
  "bank_accounts": [
    {
      "id": 1,
      "bank_name": "CRDB Bank",
      "account_name": "My Company Ltd",
      "account_number": "0150123456789",
      "swift_code": "CORUTZTZ",
      "is_default": true
    }
  ]
}

Webhook Events

Receive real-time notifications when payments are completed or fail.

When a transaction completes, PayIn sends a POST request to your configured callback URL with the transaction details. You can set a default callback URL in your dashboard, or override it per-request using the callback_url parameter.

Webhook Payload

POST → Your Callback URL{
  "event": "payin.completed",
  "request_ref": "PAYABCDEF123456",
  "external_ref": "INV-001",
  "operator_ref": "12345678",
  "receipt_number": "MPI1234567890",
  "type": "collection",
  "phone": "255714123456",
  "gross_amount": 5000,
  "platform_charge": 250,
  "operator_charge": 75,
  "net_amount": 4750,
  "currency": "TZS",
  "operator": "M-Pesa",
  "status": "completed",
  "timestamp": "2026-04-19T10:20:30Z"
}

Webhook Events

EventDescription
payin.completedA collection payment was successfully received
payout.completedA disbursement was successfully sent
settlement.createdSettlement request submitted, funds debited from collection wallet
settlement.approvedSettlement approved by admin, being processed
settlement.rejectedSettlement rejected, funds refunded to collection wallet
settlement.completedSettlement completed — bank transfer or USDT sent
transfer.approvedInternal transfer approved, funds moved to disbursement wallet
transfer.rejectedInternal transfer rejected
💡
Respond with HTTP 200. Your endpoint must return a 2xx status code within 15 seconds. If we don't receive a success response, we'll retry the webhook.

Signature Verification

Verify that webhooks are genuinely from PayIn using HMAC signatures.

Every webhook request includes two security headers:

HeaderDescription
X-Payin-SignatureHMAC-SHA256 signature of the payload
X-Payin-TimestampUnix timestamp when the webhook was sent

The signature is computed as: HMAC-SHA256(timestamp + "." + body, webhook_secret)

PHP Example

PHP$secret = 'your_webhook_secret';  // From Dashboard Settings
$signature = $_SERVER['HTTP_X_PAYIN_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_PAYIN_TIMESTAMP'] ?? '';
$body = file_get_contents('php://input');

$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (hash_equals($expected, $signature)) {
    // Webhook is authentic
    $payload = json_decode($body, true);
    // Process the payment...
} else {
    http_response_code(401);
    exit('Invalid signature');
}

Node.js Example

JavaScriptconst crypto = require('crypto');

function verifyWebhook(req, secret) {
  const signature = req.headers['x-payin-signature'];
  const timestamp = req.headers['x-payin-timestamp'];
  const body = JSON.stringify(req.body);

  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + body)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(signature)
  );
}

Python Example

Pythonimport hmac, hashlib

def verify_webhook(body, timestamp, signature, secret):
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.{body}".encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
⚠️
Always verify signatures. Never trust webhook data without verifying the HMAC signature. Find your webhook secret in the Dashboard under Settings.

Error Handling

Understand HTTP status codes and error response formats.

HTTP Status Codes

CodeMeaningDescription
200OKRequest succeeded
201CreatedResource created (collection, invoice, disbursement)
400Bad RequestMalformed request or invalid JSON body
401UnauthorizedMissing or invalid API credentials
403ForbiddenIP not whitelisted or account inactive
404Not FoundResource not found
422UnprocessableValidation failed, insufficient balance, operator down
429Too Many RequestsRate limit exceeded
502Bad GatewayInternal service temporarily unavailable
503Service UnavailableSystem under maintenance

Error Response Format

JSON{
  "message": "Human-readable error description",
  "errors": {
    "phone": ["The phone field is required."],
    "amount": ["The amount must be at least 100."]
  }
}

Payment Statuses

Every payment moves through a lifecycle of statuses.

StatusDescriptionFinal?
waitingInvoice created, waiting for customer to payNo
pendingRequest created, queued for operatorNo
processingSent to operator, awaiting confirmationNo
pending_approvalPayout awaiting maker-checker approvalNo
completedPayment successful — funds transferredYes ✓
failedTransaction failed or rejected by operatorYes ✗
expiredInvoice expired without paymentYes
rejectedInvalid amount or failed verificationYes
Poll or use webhooks. We recommend webhooks for real-time notifications. If you prefer polling, use GET /v1/status/{request_ref} — but avoid polling more than once every 5 seconds.

Rate Limits

API rate limits protect the system and ensure fair usage.

Rate limits are applied per account. When you exceed the limit, you'll receive a 429 Too Many Requests response.

TierRequests / MinuteConcurrent
Standard6010
Business30050
EnterpriseCustomCustom

Need higher limits? Contact us to discuss enterprise plans.

Supported Operators

Mobile money operators available in Tanzania.

OperatorCodePrefixesCollectionDisbursement
M-Pesa (Vodacom) mpesa 255 74/75/76
Airtel Money airtel 255 68/69/78/79
Tigo Pesa (Yas) tigopesa 255 71/65/67
Halo Pesa halopesa 255 62
📡
Operator availability may change. Use GET /v1/operators to check real-time status including maintenance windows.