python-mpesa-daraja 1.0.0.post1
A Python library for integrating with the M-Pesa payment system.
License: MIT
Requires Python: >=3.8
Keywords: mpesa, m-pesa, payment, integration, python, safaricom

# M-Pesa Python Integration Library

A simple Python library for integrating M-Pesa STK Push (Lipa Na M-Pesa Online) payments into your applications.

## Features

- Easy STK Push integration
- Automatic phone number validation and formatting
- Secure token management with auto-refresh
- Amount validation with M-Pesa limits
- Comprehensive error handling
- Kenyan phone number support

## Installation

```bash
pip install python-mpesa-daraja
```

## Requirements

- Python 3.8+
- requests
- phonenumbers

## Quick Start

```python
from python_mpesa import MpesaGateway

# Initialize the gateway
mpesa = MpesaGateway(
    business_shortcode="your_business_shortcode",
    consumer_key="your_consumer_key",
    consumer_secret="your_consumer_secret",
    pass_key="your_pass_key",
    access_token_url="https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials", # Use production URL in live environment
    stk_push_url="https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest" # Use production URL in live environment
)

# Initiate STK Push
response = mpesa.stk_push(
    phone_number="0712345678",
    amount=100,
    account_reference="account_reference", # e.g., invoice number
    transaction_type="CustomerPayBillOnline", # or "CustomerBuyGoodsOnline" for Till
    transaction_desc="Payment description", # e.g., "Payment for Order #12345"
    callback_url="your_callback_url" # e.g., "https://yourdomain.com/mpesa/callback"
)

print(response)
```

## Configuration

### Sandbox Environment

```python
ACCESS_TOKEN_URL = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"
STK_PUSH_URL = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest"
```

### Production Environment

```python
ACCESS_TOKEN_URL = "https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"
STK_PUSH_URL = "https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest"
```

## API Reference

### MpesaGateway

The main class for interacting with the M-Pesa API.

#### Initialization

```python
MpesaGateway(
    business_shortcode: str,
    consumer_key: str,
    consumer_secret: str,
    pass_key: str,
    access_token_url: str,
    stk_push_url: str
)
```

**Parameters:**
- `business_shortcode` (str): Your M-Pesa business shortcode (Paybill or Till number)
- `consumer_key` (str): Consumer key from Daraja API
- `consumer_secret` (str): Consumer secret from Daraja API
- `pass_key` (str): Lipa Na M-Pesa Online passkey
- `access_token_url` (str): OAuth token endpoint URL
- `stk_push_url` (str): STK Push API endpoint URL

#### Methods

##### `stk_push()`

Initiates an STK Push payment request.

```python
stk_push(
    phone_number: str,
    amount: int,
    account_reference: str,
    transaction_type: str,
    transaction_desc: str,
    callback_url: str
) -> dict
```

**Parameters:**
- `phone_number` (str): Customer's phone number
- `amount` (int): Amount to charge (1 - 250,000 KES)
- `account_reference` (str): Account reference (e.g., invoice number, order ID)
- `transaction_type` (str): Usually `"CustomerPayBillOnline"` for Paybill or `"CustomerBuyGoodsOnline"` for Till
- `transaction_desc` (str): Transaction description
- `callback_url` (str): URL to receive payment notifications

**Returns:**
- `dict`: Response from M-Pesa API containing:
  - `MerchantRequestID`: Unique request ID
  - `CheckoutRequestID`: Unique checkout ID
  - `ResponseCode`: Response code
  - `ResponseDescription`: Response message
  - `CustomerMessage`: Message to display to customer

**Example:**

```python
response = mpesa.stk_push(
    phone_number="0712345678",
    amount=1500.50,
    account_reference="ORDER-12345",
    transaction_type="CustomerPayBillOnline",
    transaction_desc="Payment for Order 12345",
    callback_url="https://yourdomain.com/mpesa/callback"
)
```

## Phone Number Validation

The library automatically validates and formats phone numbers for Kenyan mobile networks.

### Supported Formats

```python
# All these formats are valid and will be converted to 254712345678
"0712345678"      # Local format
"254712345678"    # International without +
"+254712345678"   # International with +
"712345678"       # Without leading zero
```

### Validation Rules

- Must be a valid Kenyan phone number
- Automatically formatted to E.164 standard (without the + prefix)

## Amount Validation

Amounts are validated against M-Pesa transaction limits.

### Rules

- Must be a positive number
- Minimum: 1 KES
- Maximum: 250,000 KES
- Decimal values are converted to integers by discarding the fractional part (e.g., 1500.75 becomes 1500)

```python
# Valid amounts
mpesa.stk_push(..., amount=100, ...)      # ✓
mpesa.stk_push(..., amount=1500.75, ...)  # ✓ Converted to 1500
mpesa.stk_push(..., amount=250000, ...)   # ✓

# Invalid amounts
mpesa.stk_push(..., amount=0, ...)        # ✗ InvalidAmountError
mpesa.stk_push(..., amount=-100, ...)     # ✗ InvalidAmountError
mpesa.stk_push(..., amount=300000, ...)   # ✗ InvalidAmountError
```

## Error Handling

The library provides comprehensive error handling with custom exceptions.

### Exception Types

#### `InvalidPhoneNumberError`

Raised when the phone number is invalid.

```python
from python_mpesa.exceptions import InvalidPhoneNumberError

try:
    response = mpesa.stk_push(
        phone_number="1234",  # Invalid
        amount=100,
        ...
    )
except InvalidPhoneNumberError as e:
    print(f"Invalid phone number: {e}")
```

#### `InvalidAmountError`

Raised when the amount is invalid.

```python
from python_mpesa.exceptions import InvalidAmountError

try:
    response = mpesa.stk_push(
        phone_number="0712345678",
        amount=-100,  # Invalid
        ...
    )
except InvalidAmountError as e:
    print(f"Invalid amount: {e}")
```

#### `MpesaConnectionError`

Raised when there's a connection error to M-Pesa servers.

```python
from python_mpesa.exceptions import MpesaConnectionError

try:
    response = mpesa.stk_push(...)
except MpesaConnectionError as e:
    print(f"Connection error: {e}")
    # Retry logic here
```

#### `PaymentError`

Raised for general payment processing errors.

```python
from python_mpesa.exceptions import PaymentError

try:
    response = mpesa.stk_push(...)
except PaymentError as e:
    print(f"Payment error: {e}")
```

### Complete Error Handling Example

```python
from python_mpesa import MpesaGateway
from python_mpesa.exceptions import (
    InvalidPhoneNumberError,
    InvalidAmountError,
    MpesaConnectionError,
    PaymentError
)

mpesa = MpesaGateway(...)

try:
    response = mpesa.stk_push(
        phone_number="0712345678",
        amount=1000,
        account_reference="INV-001",
        transaction_type="CustomerPayBillOnline",
        transaction_desc="Payment",
        callback_url="https://yourdomain.com/callback"
    )

    print(f"Success! Checkout ID: {response['CheckoutRequestID']}")

except InvalidPhoneNumberError as e:
    print(f"Invalid phone number: {e}")

except InvalidAmountError as e:
    print(f"Invalid amount: {e}")

except MpesaConnectionError as e:
    print(f"Connection error: {e}")
    # Implement retry logic

except PaymentError as e:
    print(f"Payment failed: {e}")
```

## Token Management

The library automatically handles OAuth token generation and refresh.

### Features

- Automatic token generation on initialization
- Auto-refresh before expiration (tokens last ~3400 seconds)
- Transparent to the developer

### Manual Token Refresh

While automatic refresh is built-in, you can also manually refresh:

```python
# Token is automatically refreshed when needed
# No manual intervention required
mpesa.stk_push(...)  # Token refreshed if expired
```

## Callback Handling

When a payment is processed, M-Pesa sends a callback to your specified URL.

### Example Callback Response

```json
{
  "Body": {
    "stkCallback": {
      "MerchantRequestID": "29115-34620561-1",
      "CheckoutRequestID": "ws_CO_191220191020363925",
      "ResultCode": 0,
      "ResultDesc": "The service request is processed successfully.",
      "CallbackMetadata": {
        "Item": [
          {
            "Name": "Amount",
            "Value": 1.00
          },
          {
            "Name": "MpesaReceiptNumber",
            "Value": "NLJ7RT61SV"
          },
          {
            "Name": "TransactionDate",
            "Value": 20191219102115
          },
          {
            "Name": "PhoneNumber",
            "Value": 254712345678
          }
        ]
      }
    }
  }
}
```

### Flask Example

```python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/mpesa/callback', methods=['POST'])
def mpesa_callback():
    data = request.get_json()

    callback = data['Body']['stkCallback']
    result_code = callback['ResultCode']

    if result_code == 0:
        # Payment successful
        metadata = callback['CallbackMetadata']['Item']
        receipt = next(item['Value'] for item in metadata if item['Name'] == 'MpesaReceiptNumber')
        print(f"Payment successful! Receipt: {receipt}")
    else:
        # Payment failed
        print(f"Payment failed: {callback['ResultDesc']}")

    return jsonify({"ResultCode": 0, "ResultDesc": "Success"})
```

### Django Example

```python
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json

@csrf_exempt
def mpesa_callback(request):
    if request.method == 'POST':
        data = json.loads(request.body)

        callback = data['Body']['stkCallback']
        result_code = callback['ResultCode']

        if result_code == 0:
            # Payment successful
            metadata = callback['CallbackMetadata']['Item']
            receipt = next(item['Value'] for item in metadata if item['Name'] == 'MpesaReceiptNumber')
            # Save to database

        return JsonResponse({"ResultCode": 0, "ResultDesc": "Success"})
```

## Advanced Usage

### Environment Variables

Store sensitive credentials in environment variables:

```python
import os
from python_mpesa import MpesaGateway

mpesa = MpesaGateway(
    business_shortcode=os.getenv("MPESA_SHORTCODE"),
    consumer_key=os.getenv("MPESA_CONSUMER_KEY"),
    consumer_secret=os.getenv("MPESA_CONSUMER_SECRET"),
    pass_key=os.getenv("MPESA_PASS_KEY"),
    access_token_url=os.getenv("MPESA_TOKEN_URL"),
    stk_push_url=os.getenv("MPESA_STK_PUSH_URL")
)
```

### Logging

Add logging for debugging:

```python
import logging
from python_mpesa import MpesaGateway

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

mpesa = MpesaGateway(...)

try:
    response = mpesa.stk_push(...)
    logger.info(f"STK Push initiated: {response['CheckoutRequestID']}")
except Exception as e:
    logger.error(f"STK Push failed: {str(e)}")
```

### Testing

For testing, use M-Pesa sandbox credentials:

```python
# Sandbox test credentials
BUSINESS_SHORTCODE = "174379"
PASS_KEY = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"

# Test phone numbers (use actual Safaricom numbers registered in sandbox)
TEST_PHONE = "254712345678"
```

## Best Practices

1. **Use Environment Variables**: Never hardcode credentials
2. **Implement Retry Logic**: Handle connection errors gracefully
3. **Validate Callbacks**: Verify callback authenticity using IP whitelisting
4. **Store Transaction Records**: Log all CheckoutRequestIDs for reconciliation
5. **Handle Timeouts**: Customer has 60 seconds to complete payment
6. **Test Thoroughly**: Use sandbox environment before going live

## Common Issues

### Issue: "Invalid Access Token"

**Solution**: Check that your consumer key and secret are correct.

### Issue: "Invalid Shortcode"

**Solution**: Ensure the shortcode matches your Paybill/Till number.

### Issue: "Unable to lock subscriber"

**Solution**: Customer's phone is busy with another transaction. Retry after a few seconds.

### Issue: "Timeout"

**Solution**: Customer didn't complete payment within 60 seconds. This is normal behavior.

## Response Codes

| Code | Description |
|------|-------------|
| 0 | Success |
| 1 | Insufficient Balance |
| 1032 | Request cancelled by user |
| 1037 | Timeout (user didn't enter PIN) |
| 2001 | Invalid initiator information |

## Support & Resources

- **M-Pesa Daraja API**: https://developer.safaricom.co.ke/
- **API Documentation**: https://developer.safaricom.co.ke/dashboard/apis?api=MpesaExpressSimulate
- **Sandbox Portal**: https://developer.safaricom.co.ke/dashboard/myapps

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

MIT License - see LICENSE file for details

## Changelog

### Version 1.0.0
- Initial release
- STK Push integration
- Phone number validation
- Amount validation
- Automatic token refresh
- Comprehensive error handling

---

Made with ❤️ for Developers integrating M-Pesa payments in Python applications.