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:
| Header | Description |
|---|---|
| X-API-Key | Your public API key from the dashboard |
| X-API-Secret | Your secret API key (keep this private) |
HeadersX-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-API-Secret: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
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.
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.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
| phone | string | Required | Customer phone number (10–15 digits). E.g. 255714123456 |
| amount | number | Required | Amount to collect. Min: 100, Max: 10,000,000 |
| operator | string | Required | Operator code: mpesa, airtel, tigopesa, halopesa |
| reference | string | Optional | Your internal reference. Max 100 characters. |
| description | string | Optional | Payment description. Max 255 characters. |
| currency | string | Optional | Currency code. Default: TZS |
| callback_url | string | Optional | Override 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"] }
}
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
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | number | Required | Amount to collect. Min: 100, Max: 10,000,000 |
| reference | string | Required | Customer-facing payment reference. Max 100 chars. |
| description | string | Optional | Invoice description. Max 255 characters. |
| currency | string | Optional | Currency code. Default: TZS |
| phone | string | Optional | Customer phone for reference. 10–15 digits. |
| expires_in | integer | Optional | Expiry 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..."
}
pay_url is a hosted payment page you can share with your customer. They can select their preferred operator and pay directly.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
| Parameter | Type | Required | Description |
|---|---|---|---|
| phone | string | Required | Recipient phone number. 10–15 digits. |
| amount | number | Required | Amount to send. Min: 100, Max: 10,000,000 |
| operator | string | Optional | Operator code. Auto-detected from phone if not provided. |
| reference | string | Optional | Your internal reference. Max 100 characters. |
| description | string | Optional | Payout description. Max 255 characters. |
| currency | string | Optional | Currency code. Default: TZS |
| callback_url | string | Optional | Override 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
}
Check the current status of any payment request — collection, invoice, or disbursement.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| request_ref | string | The 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"
}
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"
}
Retrieve a paginated list of your transactions with optional filtering by date, status, type, and operator.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| date_from | string | — | Filter from date (YYYY-MM-DD) |
| date_to | string | — | Filter to date (YYYY-MM-DD) |
| status | string | — | completed, failed, processing, pending |
| type | string | — | collection, disbursement, manual_c2b |
| operator | string | — | Filter by operator code |
| per_page | integer | 20 | Results 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
}
}
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"
}
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 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
| Event | Description |
|---|---|
| payin.completed | A collection payment was successfully received |
| payout.completed | A disbursement was successfully sent |
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:
| Header | Description |
|---|---|
| X-Payin-Signature | HMAC-SHA256 signature of the payload |
| X-Payin-Timestamp | Unix 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)
Error Handling
Understand HTTP status codes and error response formats.
HTTP Status Codes
| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created (collection, invoice, disbursement) |
| 400 | Bad Request | Malformed request or invalid JSON body |
| 401 | Unauthorized | Missing or invalid API credentials |
| 403 | Forbidden | IP not whitelisted or account inactive |
| 404 | Not Found | Resource not found |
| 422 | Unprocessable | Validation failed, insufficient balance, operator down |
| 429 | Too Many Requests | Rate limit exceeded |
| 502 | Bad Gateway | Internal service temporarily unavailable |
| 503 | Service Unavailable | System 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.
| Status | Description | Final? |
|---|---|---|
waiting | Invoice created, waiting for customer to pay | No |
pending | Request created, queued for operator | No |
processing | Sent to operator, awaiting confirmation | No |
pending_approval | Payout awaiting maker-checker approval | No |
completed | Payment successful — funds transferred | Yes ✓ |
failed | Transaction failed or rejected by operator | Yes ✗ |
expired | Invoice expired without payment | Yes |
rejected | Invalid amount or failed verification | Yes |
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.
| Tier | Requests / Minute | Concurrent |
|---|---|---|
| Standard | 60 | 10 |
| Business | 300 | 50 |
| Enterprise | Custom | Custom |
Need higher limits? Contact us to discuss enterprise plans.
Supported Operators
Mobile money operators available in Tanzania.
| Operator | Code | Prefixes | Collection | Disbursement |
|---|---|---|---|---|
| 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 | ✓ | ✓ |
GET /v1/operators to check real-time status including maintenance windows.