raqm-core
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 raise
PaymentErroron business-level failures instead of returning a result with a non-success response code. Wrap calls in try/except to handle errors gracefully.
Supported Gateways
| Gateway | Status |
|---|---|
| EasyPaisa | ✅ Live |
| JazzCash | ✅ Live (MWALLET) |
| HBL | 📅 Planned |
| ABL | 📅 Planned |
| UBL | 📅 Planned |
| … | 📅 Your gateway here |
Features
- 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
Installation
pip install raqm-core
Or with uv:
uv add raqm-core
Requires Python 3.9+.
Quick Start
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())
Usage
EasyPaisa
Initialisation
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)
Mobile Account (MA) Payment
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 # str
Over-the-Counter (OTC) Payment
result = 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 # str
Transaction Inquiry
result = 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")
Error Handling
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.
Architecture
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
Adding a New Gateway
Want to add support for a new processor? Follow this checklist.
1. Research the API
Understand the gateway's:
- Authentication mechanism (Basic Auth, HMAC, API key, etc.)
- Endpoints and request/response formats
- Error/response codes
2. Create header helpers
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.
3. Create Pydantic schemas
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(...)
4. Create the client class
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:
...
5. Write integration tests
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(...)
6. Update this README
Add the new gateway to the Supported Gateways table with the appropriate status badge.
Roadmap
- 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
Development
# 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
pytest
Contributing
Contributions are welcome! See Adding a New Gateway for the detailed guide.
Please open an issue first to discuss your proposed changes.
License
MIT
Metadata
Release files for raqm-core 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| raqm_core-0.3.0.tar.gz | 12.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| raqm_core-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 22.5 kB
Release files / raqm_core-0.3.0.tar.gz
| Download URL | raqm_core-0.3.0.tar.gz |
|---|---|
| Size | 12.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b5ae809824d2e32866b754a5af0580d882a6b7ef55938f38c98a895dbb510377
|
|
BLAKE2b-256 checksum How to use checksums |
5173daa4f50f43b5f4141a865a37743be9cca412cffcdd997d5436de2213a0f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.5
|
Release files / raqm_core-0.3.0-py3-none-any.whl
| Download URL | raqm_core-0.3.0-py3-none-any.whl |
|---|---|
| Size | 10.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d4ba997a90c20ad4a62054f59bb8cd1806497cb213139eb3e1e8b35f7b339138
|
|
BLAKE2b-256 checksum How to use checksums |
022182c73e950ec70de1af5d3034c33733ed629d6677aa2f9846f67d48b1e4b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.5
|