Lyntra Payments API
Welcome to the Lyntra Payments API documentation. Our REST API lets you integrate mobile money collections and disbursements via MTN Mobile Money and Airtel Money across Uganda.
https://api.lyntra.com/v1 ยท Staging: https://staging.lyntraengineering.com/v1
How the API Works
All requests use standard HTTP methods (GET, POST, PATCH, DELETE) and return JSON responses. Authentication uses API keys passed in the Authorization header.
Request Format
POST /v1/collections/initiate HTTP/1.1
Host: api.lyntra.com
Authorization: Bearer pgk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{"msisdn": "256771234567", "amount": "10000", ...}
Response Format
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reference": "LYN_20240326_XYZ789",
"status": "PENDING",
"amount": "10000",
"currency": "UGX",
"created_at": "2024-03-26T10:00:00Z"
}
Error Format
{
"error": {
"code": "INVALID_MSISDN",
"message": "The provided phone number is not a valid MTN number",
"request_id": "req_abc123"
}
}
Authentication
All API requests require an API key passed in the HTTP Authorization header as a Bearer token.
API Key Types
- Sandbox keys โ prefix
pgk_sandbox_โ safe for testing, no real money moves - Production keys โ prefix
pgk_live_โ live transactions, real money
Scopes
API keys have scopes that control what operations they can perform:
| Scope | Description |
|---|---|
collections:write | Initiate payment collections |
collections:read | View collection transactions |
disbursements:write | Initiate disbursements |
disbursements:read | View disbursement transactions |
balance:read | View account balances |
kyc:read | Check KYC / account verification status |
Example Header
Authorization: Bearer pgk_sandbox_b9ac76b458a85f8bd15d9e276a14bb8f7c3f127a498386593b5b124023e96d60
Getting API Keys
API keys are generated from your dashboard under API Keys. You can create multiple keys with different scopes and environments. Production keys require KYC verification.
Quick Start
Get your first payment collection running in under 5 minutes.
Step 1 โ Get your API key
Log in to your dashboard, navigate to API Keys, and create a sandbox key with collections:write scope.
Step 2 โ Get your Business Account ID
Go to Business Accounts in your dashboard and copy the ID of the account you want to receive payments into. Business accounts act as virtual wallets โ credits from collections and debits from disbursements are tracked against them.
Step 3 โ Initiate a collection
The correct mobile money provider (MTN or Airtel) is automatically detected from the customer's phone number โ you do not need to specify it.
const API_KEY = 'pgk_sandbox_your_key_here';
const ACCOUNT_ID = 'your_business_account_id';
const res = await fetch('https://staging.lyntraengineering.com/v1/collections/initiate', {
method: 'POST',
headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
msisdn: '256771234567', // MTN auto-detected from 0771 prefix
amount: '5000', // Amount in UGX
currency: 'UGX',
reference: 'ORDER-1234', // Your unique reference
business_account_id: ACCOUNT_ID,
description: 'Payment for Order #1234'
})
});
const data = await res.json();
console.log(data.reference); // Save this to check status laterimport requests
API_KEY = 'pgk_sandbox_your_key_here'
ACCOUNT_ID = 'your_business_account_id'
r = requests.post(
'https://staging.lyntraengineering.com/v1/collections/initiate',
headers={'Authorization': f'Bearer {API_KEY}'},
json={
'msisdn': '256771234567', # MTN auto-detected from 0771 prefix
'amount': '5000',
'currency': 'UGX',
'reference': 'ORDER-1234',
'business_account_id': ACCOUNT_ID,
'description': 'Payment for Order #1234'
}
)
data = r.json()
print(data['reference']) # Save to check status$apiKey = 'pgk_sandbox_your_key_here';
$accountId = 'your_business_account_id';
$ch = curl_init('https://staging.lyntraengineering.com/v1/collections/initiate');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer {$apiKey}", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'msisdn' => '256771234567', // MTN auto-detected
'amount' => '5000',
'currency' => 'UGX',
'reference' => 'ORDER-1234',
'business_account_id' => $accountId,
'description' => 'Payment for Order #1234'
])
]);
$data = json_decode(curl_exec($ch), true);
echo $data['reference']; // Save to check statuscurl -X POST https://staging.lyntraengineering.com/v1/collections/initiate \
-H "Authorization: Bearer pgk_sandbox_your_key" \
-H "Content-Type: application/json" \
-d '{
"msisdn": "256771234567",
"amount": "5000",
"currency": "UGX",
"reference": "ORDER-1234",
"business_account_id": "your_account_id",
"description": "Payment for Order #1234"
}'Step 4 โ Handle the webhook
When the customer pays (or the transaction fails), Lyntra sends a POST request to your callback_url:
{
"event": "collection.successful",
"data": {
"reference": "LYN_20240326_XYZ789",
"status": "SUCCESSFUL",
"amount": "5000",
"msisdn": "256771234567",
"provider_txn_id": "MTN_TXN_987654"
},
"timestamp": "2024-03-26T10:05:23Z",
"signature": "sha256=abc123..."
}
Sandbox Testing
https://staging.lyntraengineering.com/v1 with keys prefixed pgk_sandbox_. No real money moves.
The sandbox mimics the full production behaviour. Use these test numbers to simulate different outcomes:
| Phone Number | Provider | Simulated Outcome |
|---|---|---|
256771234567 | MTN | SUCCESSFUL |
256772000001 | MTN | FAILED โ Insufficient funds |
256772000002 | MTN | FAILED โ Number not registered |
256701234567 | AIRTEL | SUCCESSFUL |
256702000001 | AIRTEL | FAILED โ Timeout |
Set environment: "SANDBOX" in all requests during testing. Switch to "PRODUCTION" when you go live.
Collections
Collections let you request a payment from a customer's mobile money wallet. The customer receives a USSD prompt to approve the payment.
Sends a mobile money debit prompt to the customer's phone. The response returns immediately with a PROCESSING status. The final result is delivered via webhook. The mobile money provider (MTN or Airtel) is automatically selected based on the customer's phone number prefix โ no provider configuration required.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| msisdn | string | required | Customer's phone number. Format: 256XXXXXXXXX. Provider auto-detected from prefix. |
| amount | string | required | Amount in the specified currency (e.g. "5000") |
| business_account_id | string (UUID) | required | The business account to receive the funds |
| currency | string | optional | Currency code. Defaults to "UGX" |
| reference | string | optional | Your own unique reference ID for idempotency. Auto-generated if omitted. |
| description | string | optional | Payment description shown to the customer |
Response
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reference": "LYN_20240326_ABC123",
"status": "PENDING",
"type": "COLLECTION",
"provider": "MTN",
"msisdn": "256771234567",
"amount": "5000",
"currency": "UGX",
"fee": "75",
"net_amount": "4925",
"initiated_at": "2024-03-26T10:00:00Z"
}
Retrieve the current status and details of a collection transaction.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| reference | string | The transaction reference returned from the initiate call |
Response
{
"reference": "LYN_20240326_ABC123",
"status": "SUCCESSFUL",
"provider_txn_id": "MTN_TXN_987654",
"completed_at": "2024-03-26T10:02:45Z",
"amount": "5000", "fee": "75", "net_amount": "4925"
}
Returns a paginated list of all collection transactions for your business.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number |
| page_size | integer | 50 | Results per page (max 200) |
| status | string | โ | Filter by status: PENDING, SUCCESSFUL, FAILED, CANCELLED |
| provider | string | โ | Filter by provider: MTN, AIRTEL |
Disbursements
Disbursements let you send money to any MTN or Airtel mobile money number. Use this for payroll, refunds, agent commissions, and bulk payouts.
The mobile money provider is automatically selected from the recipient's phone number โ no additional configuration needed.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| msisdn | string | required | Recipient's phone number (256XXXXXXXXX). Provider auto-detected from prefix. |
| amount | string | required | Amount to send |
| business_account_id | string (UUID) | required | The business account to debit |
| currency | string | optional | Defaults to "UGX" |
| reference | string | optional | Your unique reference for idempotency |
| recipient_name | string | optional | Recipient's name for records |
| description | string | optional | Transaction description |
Code Examples
const res = await fetch('https://api.lyntra.com/v1/disbursements/initiate', {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
msisdn: '256771234567', // MTN auto-detected from 0771 prefix
amount: '10000',
currency: 'UGX',
reference: 'SALARY-MAR-2024-001',
business_account_id: accountId,
recipient_name: 'John Doe',
description: 'Salary payment March 2024'
})
});r = requests.post('https://api.lyntra.com/v1/disbursements/initiate',
headers={'Authorization': f'Bearer {api_key}'},
json={
'msisdn': '256771234567', # MTN auto-detected
'amount': '10000',
'currency': 'UGX',
'reference': 'SALARY-MAR-2024-001',
'business_account_id': account_id,
'recipient_name': 'John Doe',
'description': 'Salary payment'
})$res = json_decode(file_get_contents('https://api.lyntra.com/v1/disbursements/initiate',
false, stream_context_create(['http' => [
'method' => 'POST',
'header' => "Authorization: Bearer {$apiKey}\r\nContent-Type: application/json",
'content' => json_encode([
'msisdn' => '256771234567', // MTN auto-detected
'amount' => '10000',
'currency' => 'UGX',
'reference' => 'SALARY-MAR-2024-001',
'business_account_id' => $accountId,
'recipient_name' => 'John Doe',
])]])), true);Retrieve the current status of a disbursement by your reference. Also accessible by internal UUID at /v1/disbursements/txn/{id}.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reference": "SALARY-MAR-2024-001",
"status": "SUCCESSFUL",
"provider": "MTN",
"msisdn": "256771234567",
"amount": "10000",
"currency": "UGX",
"provider_txn_id": "MTN_TXN_123456",
"completed_at": "2024-03-26T10:04:12Z"
}
Returns a paginated list of all disbursement transactions for your business.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number |
| page_size | integer | 50 | Results per page (max 200) |
| status | string | โ | Filter: PENDING, PROCESSING, SUCCESSFUL, FAILED |
| provider | string | โ | Filter: MTN, AIRTEL |
Manually polls the provider for the current disbursement status and updates the transaction record. Useful if a callback was missed.
KYC โ Know Your Customer
KYC endpoints let you verify mobile money subscribers before initiating transactions. Results are cached for 24 hours to reduce provider calls. Requires the kyc:read scope.
Returns full subscriber information from the mobile money provider. The provider is auto-detected from the phone number prefix.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| msisdn | string | Phone number in format 256XXXXXXXXX |
Response
{
"msisdn": "256771234567",
"first_name": "John",
"last_name": "Doe",
"gender": "M",
"birthdate": "1990-01-15",
"is_active": true,
"is_barred": false,
"is_pin_set": true,
"registration_status": "REGISTERED",
"provider": "MTN"
}
Returns a lightweight response indicating whether the subscriber can transact. Active, non-barred subscribers return can_transact: true.
{
"msisdn": "256771234567",
"can_transact": true,
"is_active": true,
"is_barred": false
}
Business Accounts
Business accounts are virtual wallets that track your transaction flows. Credits from successful collections and debits from disbursements are recorded against the account's shadow ledger.
Returns all business accounts for your company.
[
{
"id": "ba_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "Main Operations",
"currency": "UGX",
"is_active": true,
"created_at": "2024-03-26T09:00:00Z"
}
]
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | required | Account display name |
| currency | string | optional | Defaults to "UGX" |
| description | string | optional | Internal description |
Update the name, description, or active status of an account.
| Parameter | Type | Description |
|---|---|---|
| name | string | New display name |
| description | string | Updated description |
| is_active | boolean | Set to false to deactivate |
Balance
Returns shadow ledger balances for a business account, broken down by provider. Requires balance:read scope. Use /v1/balance/{business_account_id} to query a specific account.
{
"business_account_id": "ba_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"account_name": "Main Operations",
"currency": "UGX",
"providers": [
{
"provider": "MTN",
"account_type": "COLLECTION",
"shadow_balance": 2500000
},
{
"provider": "MTN",
"account_type": "DISBURSEMENT",
"shadow_balance": 750000
},
{
"provider": "AIRTEL",
"account_type": "COLLECTION",
"shadow_balance": 980000
}
]
}
Webhooks
Lyntra Payments sends real-time webhook events to your callback_url when transaction statuses change. This is the recommended way to track payment outcomes.
Webhook Events
| Event | Description |
|---|---|
collection.successful | Customer paid successfully |
collection.failed | Collection failed (declined, timeout, etc.) |
collection.cancelled | Customer cancelled the prompt |
disbursement.successful | Money sent successfully |
disbursement.failed | Disbursement failed |
Webhook Payload
{
"event": "collection.successful",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reference": "LYN_20240326_ABC123",
"status": "SUCCESSFUL",
"type": "COLLECTION",
"provider": "MTN",
"msisdn": "256771234567",
"amount": "5000",
"currency": "UGX",
"fee": "75",
"net_amount": "4925",
"provider_txn_id": "MTN_TXN_987654",
"completed_at": "2024-03-26T10:02:45Z"
},
"timestamp": "2024-03-26T10:02:46Z",
"signature": "sha256=4ac3f7e89b1234567890abcdef123456"
}
Verifying Webhook Signatures
Each webhook includes a signature header computed as HMAC-SHA256(payload, webhook_secret). Always verify this before processing.
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(signatureHeader)
);
}
// Express.js example
app.post('/webhooks', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['x-lyntra-signature'];
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event...
res.sendStatus(200);
});import hmac, hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks', methods=['POST'])
def webhook():
sig = request.headers.get('X-Lyntra-Signature', '')
if not verify_webhook(request.data, sig, WEBHOOK_SECRET):
abort(401)
event = request.get_json()
# Process event...
return '', 200function verifyWebhook($rawBody, $signature, $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
$rawBody = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_LYNTRA_SIGNATURE'] ?? '';
if (!verifyWebhook($rawBody, $sig, $webhookSecret)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($rawBody, true);
// Process $event...
http_response_code(200);data.id field before acting on it.
Error Codes
| HTTP | Code | Description |
|---|---|---|
| 400 | INVALID_MSISDN | Phone number format is invalid |
| 400 | INVALID_AMOUNT | Amount must be a positive number |
| 400 | UNSUPPORTED_PROVIDER | Provider not supported for this number |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 403 | INSUFFICIENT_SCOPES | API key lacks required scope |
| 403 | IP_BLOCKED | Request from non-whitelisted IP |
| 404 | NOT_FOUND | Transaction or resource not found |
| 409 | DUPLICATE_REFERENCE | A transaction with this reference already exists |
| 422 | VALIDATION_ERROR | Request body failed validation |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests. Retry after the specified delay |
| 502 | PROVIDER_ERROR | Error from mobile money provider |
| 503 | PROVIDER_UNAVAILABLE | Provider temporarily unavailable |
API Keys
Create and manage API keys from your dashboard under API Keys. Each key has a set of scopes and is bound to an environment (sandbox or production).
Requires a valid JWT session (login first). The raw key is only returned once at creation โ store it securely.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | required | Display label for the key |
| environment | string | required | "SANDBOX" or "PRODUCTION" |
| scopes | string[] | required | List of scopes, e.g. ["collections:write","collections:read"] |
{
"id": "3fa85f64-...",
"name": "Production Key",
"raw_key": "pgk_live_xxxxxxxxxxxxxxxxxxxxxxxx", // Only shown once!
"key_prefix": "pgk_live_xxxx",
"scopes": ["collections:write", "collections:read"],
"environment": "PRODUCTION",
"is_active": true
}
Providers & Networks
The correct mobile money provider is automatically selected based on the customer's phone number prefix. You never specify a provider explicitly in collection or disbursement requests.
| Prefix | Provider | Network | Collections | Disbursements | KYC |
|---|---|---|---|---|---|
2567 (070x) | AIRTEL | Airtel Money | โ | โ | โ |
2567 (071x) | MTN | MTN Mobile Money | โ | โ | โ |
2567 (077x) | MTN | MTN Mobile Money | โ | โ | โ |
2567 (075x) | AIRTEL | Airtel Money | โ | โ | โ |
| Other | โ | Unsupported | โ | โ | โ |
422 UNSUPPORTED_NETWORK. Always validate the MSISDN format before initiating a transaction.
Platform-Managed Credentials
MTN and Airtel API credentials (client ID, client secret, subscription keys) are stored at the platform level. All business accounts share these platform provider configurations โ there is no per-company or per-account credential management.
Transaction Statuses
| Status | Meaning | Terminal? |
|---|---|---|
| PENDING | Transaction initiated, waiting for customer | No |
PROCESSING | Provider is processing | No |
| SUCCESSFUL | Transaction completed successfully | โ Yes |
| FAILED | Transaction failed | โ Yes |
| CANCELLED | Cancelled by customer | โ Yes |
| EXPIRED | Customer didn't respond within time limit | โ Yes |
SDKs & Libraries
Official SDKs coming soon. Until then, use the REST API directly with the code examples above.
Changelog
v1.1 โ March 2026
- Platform-level provider configs โ MTN and Airtel credentials are now managed centrally by Lyntra. Companies no longer configure per-account provider credentials.
- Automatic provider detection โ The
providerandenvironmentfields have been removed from collection and disbursement request bodies. The correct provider is selected automatically from the phone number prefix. - KYC endpoints โ New
GET /v1/kyc/{msisdn}for full subscriber lookup andGET /v1/kyc/{msisdn}/validatefor quick transaction eligibility checks. Results cached for 24 hours. - Disbursement sync โ New
POST /v1/disbursements/{reference}/syncendpoint to manually poll the provider for the latest status. - Per-provider balance breakdown โ
GET /v1/balance/{business_account_id}now returns balances split by provider and account type (collection vs disbursement).
v1.0 โ January 2026
- Initial release of Lyntra Payments API
- MTN Mobile Money collections and disbursements
- Airtel Money collections and disbursements
- Real-time webhooks with HMAC signature verification
- Sandbox environment for testing
- Business account management
- Scoped API keys