Skip to main content

Unified Python SDK for Pakistani payment gateways (EasyPaisa, JazzCash, HBL, ABL, UBL, etc.)

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

raqm_core-0.3.0.tar.gz (12.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

raqm_core-0.3.0-py3-none-any.whl (10.2 kB view details)

Uploaded Python 3

File details

Details for the file raqm_core-0.3.0.tar.gz.

File metadata

  • Download URL: raqm_core-0.3.0.tar.gz
  • Upload date:
  • Size: 12.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for raqm_core-0.3.0.tar.gz
Algorithm Hash digest
SHA256 b5ae809824d2e32866b754a5af0580d882a6b7ef55938f38c98a895dbb510377
MD5 2b14b2069dbabf79d6b736a56e402917
BLAKE2b-256 5173daa4f50f43b5f4141a865a37743be9cca412cffcdd997d5436de2213a0f6

See more details on using hashes here.

File details

Details for the file raqm_core-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: raqm_core-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 10.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for raqm_core-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d4ba997a90c20ad4a62054f59bb8cd1806497cb213139eb3e1e8b35f7b339138
MD5 ed84e8c5140c79d0ba62e8f3259aefd0
BLAKE2b-256 022182c73e950ec70de1af5d3034c33733ed629d6677aa2f9846f67d48b1e4b5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page