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

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
operatorstringRequiredOperator code: mpesa, airtel, tigopesa, halopesa
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
    }
  ]
}

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