Unified Python SDK for Pakistani payment gateways.
One async, type-safe interface across multiple processors — EasyPaisa, JazzCash, HBL, ABL, UBL, and more.
⚠️ Work in progress. EasyPaisa and JazzCash (MWALLET) are implemented. Contributions welcome!
⚠️ Breaking change in v0.2.4 — methods now raisePaymentErroron business-level failures instead of returning a result with a non-success response code. Wrap calls in try/except to handle errors gracefully.
| Gateway | Status |
|---|---|
| EasyPaisa | ✅ Live |
| JazzCash | ✅ Live (MWALLET) |
| HBL | 📅 Planned |
| ABL | 📅 Planned |
| UBL | 📅 Planned |
| … | 📅 Your gateway here |
- Async-first — built on
httpx.AsyncClientfor non-blocking I/O - Type-safe — all responses are validated through Pydantic models
- Consistent API — same patterns across all gateways
- Mock-friendly — inject your own
httpx.AsyncClientfor testing - Minimal dependencies — just
httpxandpydantic
pip install raqm-coreOr with uv:
uv add raqm-coreRequires Python 3.9+.
import asyncio
from raqm_core import EasyPaisa
async def main():
ep = EasyPaisa(
store_id="43",
username="your_username",
password="your_password",
sandbox=True,
)
result = await ep.pay_via_ma(
order_id="order_123",
amount="1000.00",
email="customer@example.com",
mobile_number="03451234567",
)
print(f"Status: {result.responseCode} — {result.responseDesc}")
print(f"Transaction ID: {result.transactionId}")
asyncio.run(main())from raqm_core import EasyPaisa
ep = EasyPaisa(
store_id="43",
username="your_username",
password="your_password",
sandbox=True, # set False for production
)You can optionally inject your own httpx.AsyncClient — useful for testing with httpx.MockTransport:
import httpx
client = httpx.AsyncClient(...)
ep = EasyPaisa(store_id="43", username="...", password="...", sandbox=True, client=client)result = await ep.pay_via_ma(
order_id="order_123",
amount="500.00",
email="customer@example.com",
mobile_number="03451234567",
)
# EasyPaisaMAResponse fields:
result.orderId # str
result.storeId # int
result.transactionId # str
result.transactionDateTime # str (dd/MM/yyyy hh:mm AM/PM)
result.responseCode # EasypaisaResponseCode (enum)
result.responseDesc # strresult = await ep.pay_via_otc(
order_id="order_456",
amount="1500.00",
email="customer@example.com",
msisdn="03451234567",
token_expiry="01/01/2025 11:59 PM",
)
# EasyPaisaOTCResponse fields (extends base):
result.paymentToken # str
result.paymentTokenExpiryDateTime # strresult = await ep.inquire_transaction_status(
order_id="order_123",
account_number="123456789",
)
# EasyPaisaInquireTransactionResponse fields:
result.transactionStatus # str (e.g. "COMPLETED")
result.transactionAmount # str
result.accountNum # str
result.storeName # str
result.msisdn # str
result.paymentMode # str ("MA", "OTC", "CC")All gateway methods raise typed exceptions on failure:
RaqmCoreError # Base exception for the SDK
├── NetworkError # HTTP/transport errors (connection, timeout, 4xx/5xx)
└── PaymentError # Business-level failures (non-success response code)
# .response — the full gateway response object
from raqm_core.exceptions.exceptions import NetworkError, PaymentError
try:
result = await ep.pay_via_ma(...)
except NetworkError as e:
print(f"Network issue: {e}")
except PaymentError as e:
print(f"Payment failed: {e}")
print(f"Response code: {e.response.responseCode}")PaymentError carries the gateway's response object in exc.response, so you can inspect the raw error details even when the SDK raises.
Each payment gateway follows a consistent 3-layer structure:
src/
├── <gateway>.py # Client class — public API
├── headers/
│ └── <gateway>.py # Auth / signing helpers
└── schemas/
└── <gateway>.py # Pydantic request/response models
Current structure:
src/
├── easypaisa.py # EasyPaisa client
├── jazzcash.py # JazzCash client
├── exceptions/
│ └── exceptions.py # Custom exception hierarchy
├── headers/
│ ├── easypaisa.py # Basic Auth header
│ └── jazzcash.py # SHA-256 secure hash
└── schemas/
├── easypaisa.py # EasyPaisa Pydantic models
└── jazzcash.py # JazzCash Pydantic models
tests/
├── conftest.py # Shared fixtures & mock helpers
├── test_easypaisa.py # EasyPaisa integration tests
├── test_jazzcash.py # JazzCash integration tests
└── headers/
├── test_easypaisa.py # Auth header unit tests
└── test_jazzcash.py # Secure hash unit tests
Want to add support for a new processor? Follow this checklist.
Understand the gateway's:
- Authentication mechanism (Basic Auth, HMAC, API key, etc.)
- Endpoints and request/response formats
- Error/response codes
src/headers/<gateway>.py — authentication or signing logic.
# src/headers/hbl.py
def generate_signature(api_key: str, payload: dict) -> str:
...Write unit tests in tests/headers/test_<gateway>.py.
src/schemas/<gateway>.py — response models and enums.
# src/schemas/hbl.py
from pydantic import BaseModel, Field
class HBLResponse(BaseModel):
orderId: str = Field(...)
responseCode: str = Field(...)
responseDesc: str = Field(...)src/<gateway>.py — async client with a _post() helper and public methods.
# src/hbl.py
import httpx
from .headers.hbl import generate_signature
from .schemas.hbl import HBLResponse
class HBL:
def __init__(self, api_key: str, sandbox: bool, client: httpx.AsyncClient | None = None):
self._client = client or httpx.AsyncClient()
...
async def _post(self, endpoint: str, payload: dict) -> dict:
...
async def pay(self, order_id: str, amount: str, ...) -> HBLResponse:
...tests/test_<gateway>.py — use httpx.MockTransport to mock responses. Add shared helpers to tests/conftest.py.
conftest.py — add a success body constant and a factory:
# tests/conftest.py
from raqm_core.hbl import HBL
HBL_SUCCESS_BODY = {
"orderId": "abc123",
"responseCode": "0000",
"responseDesc": "SUCCESS",
}
def make_hbl(response_body: dict) -> HBL:
return HBL(
api_key="test",
sandbox=True,
client=make_client(response_body),
)test file — import from conftest:
# tests/test_hbl.py
import pytest
from raqm_core.exceptions.exceptions import PaymentError
from tests.conftest import HBL_SUCCESS_BODY, make_client, make_hbl
class TestPay:
@pytest.mark.asyncio
async def test_success(self):
hbl = make_hbl(HBL_SUCCESS_BODY)
result = await hbl.pay(...)
assert result.responseCode == "0000"
@pytest.mark.asyncio
async def test_network_error(self):
hbl = HBL(
api_key="test",
sandbox=True,
client=make_client({"error": "timeout"}, status_code=500),
)
with pytest.raises(NetworkError):
await hbl.pay(...)Add the new gateway to the Supported Gateways table with the appropriate status badge.
- EasyPaisa (MA, OTC, inquiry)
- JazzCash client + schemas (MWALLET)
- HBL payment gateway
- ABL payment gateway
- UBL payment gateway
- Standardised error handling across gateways
- Request/response logging middleware
- CI/CD + automated testing
# Clone the repo
git clone https://github.com/your-username/raqm-core.git
cd raqm-core
# Create a virtual environment
uv venv
source .venv/bin/activate
# Install dependencies
uv sync
# Run tests
pytestContributions are welcome! See Adding a New Gateway for the detailed guide.
Please open an issue first to discuss your proposed changes.
MIT