Skip to main content

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 PaymentError on 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.AsyncClient for 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.AsyncClient for testing
  • Minimal dependencies — just httpx and pydantic

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)

Source distribution for raqm-core 0.3.0
File Size Uploaded
raqm_core-0.3.0.tar.gz 12.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for raqm-core 0.3.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page