Skip to main content

paylinker

PyPI version Python versions License: MIT Typed

Framework-agnostic payment integration SDK for Uzbekistan payment providers.

Currently supported: Click (SHOP-API + Merchant API), Payme (JSON-RPC Merchant API)

Planned: Uzum, and more.

Features

  • Pure Python — works with Django, FastAPI, Flask, Celery, or plain scripts
  • Sync and async HTTP clients
  • Pydantic v2 schema validation
  • Unified exception hierarchy
  • Click: Signature verification, webhook handler, Merchant API client
  • Payme: JSON-RPC handler, Basic Auth, full transaction lifecycle
  • Fully typed (PEP 561 compatible)

Installation

pip install paylinker

Quick Start

1. Payment URL generation

from paylinker import ClickConfig, ClickProvider

config = ClickConfig(
    merchant_id=12345,
    service_id=67890,
    secret_key="your-secret-key",
)
provider = ClickProvider(config)

url = provider.generate_payment_url(
    amount=50000.0,
    transaction_param="order-42",
    return_url="https://yoursite.com/done",
)
# Redirect user to this URL

2. Webhook handling (SHOP-API)

from paylinker import ClickConfig, ClickWebhookHandler
from paylinker.providers.click.webhook import OrderCheckResult
from paylinker.providers.click.enums import ClickErrorCode

class MyOrderValidator:
    def check_order(self, merchant_trans_id, amount):
        order = db.get_order(merchant_trans_id)
        if not order:
            return OrderCheckResult.fail(ClickErrorCode.ORDER_NOT_FOUND, "Not found")
        if order.is_paid:
            return OrderCheckResult.fail(ClickErrorCode.ALREADY_PAID, "Already paid")
        if float(order.amount) != float(amount):
            return OrderCheckResult.fail(ClickErrorCode.INCORRECT_AMOUNT, "Wrong amount")
        return OrderCheckResult.ok()

    def on_prepare(self, click_trans_id, merchant_trans_id, amount):
        return db.create_transaction(click_trans_id, merchant_trans_id, amount).id

    def on_complete(self, click_trans_id, merchant_trans_id, merchant_prepare_id, amount):
        db.confirm_payment(merchant_prepare_id)
        return merchant_prepare_id

    def on_cancelled(self, click_trans_id, merchant_trans_id, merchant_prepare_id):
        db.cancel_transaction(merchant_prepare_id)

handler = ClickWebhookHandler(config, MyOrderValidator())

# In any framework:
result = handler.handle(request_data)  # dict in, dict out

3. Merchant API (server-to-server)

from paylinker import ClickConfig, ClickMerchantClient

config = ClickConfig(
    merchant_id=12345,
    service_id=67890,
    secret_key="shop-secret",
    merchant_user_id=111,
    merchant_api_secret="api-secret",
)

with ClickMerchantClient(config) as client:
    invoice = client.create_invoice(
        amount=50000.0,
        phone_number="998901234567",
        merchant_trans_id="order-42",
    )
    print(invoice.invoice_id)

Async version:

from paylinker import AsyncClickMerchantClient

async with AsyncClickMerchantClient(config) as client:
    invoice = await client.create_invoice(...)

4. Payme — JSON-RPC handling

from paylinker import PaymeConfig, PaymeRpcHandler
from paylinker.providers.payme.rpc_handler import PaymeError
from paylinker.providers.payme.schemas import CheckPerformResult, CreateTransactionResult

config = PaymeConfig(
    merchant_id="your-merchant-id",
    merchant_key="your-key",
)

class MyTransactionHandler:
    def check_perform(self, amount, account):
        return CheckPerformResult(allow=True)
    def create_transaction(self, payme_id, payme_time, amount, account):
        ...  # Create and return CreateTransactionResult
    def perform_transaction(self, payme_id): ...
    def cancel_transaction(self, payme_id, reason): ...
    def check_transaction(self, payme_id): ...
    def get_statement(self, from_time, to_time): ...

handler = PaymeRpcHandler(config, MyTransactionHandler())

# In any framework — always return HTTP 200:
result = handler.handle(request_body_dict, authorization_header)

5. Payme — Payment URL

from paylinker import PaymeConfig, PaymeProvider

provider = PaymeProvider(config)
url = provider.generate_payment_url(
    amount=5000000,  # 50,000 UZS in tiyin
    transaction_param="order-123",
)

Project Structure

src/paylinker/
├── __init__.py          # Public API
├── config/              # Provider configuration (Pydantic)
├── clients/             # Sync + async HTTP clients
├── providers/
│   ├── _base.py         # Abstract base provider
│   ├── click/           # Click provider
│   │   ├── provider.py  # Payment URL generation
│   │   ├── webhook.py   # SHOP-API webhook handler
│   │   ├── merchant.py  # Merchant API client
│   │   ├── schemas.py   # Request/response models
│   │   ├── signing.py   # Signature utilities
│   │   └── enums.py     # Error codes, action types
│   └── payme/           # Payme provider
│       ├── provider.py  # Payment URL generation (GET + POST)
│       ├── rpc_handler.py  # JSON-RPC method dispatcher
│       ├── auth.py      # Basic Auth verification
│       ├── schemas.py   # JSON-RPC request/response models
│       └── enums.py     # Transaction states, error codes
├── schemas/             # Base schema classes
├── exceptions/          # Unified exception hierarchy
├── types/               # Shared type aliases
└── utils/               # Hashing utilities

Development

pip install -e ".[dev]"
pytest
ruff check src/ tests/
mypy src/

Building & Publishing

This package uses Hatchling and a single source of truth for the version (src/paylinker/_version.py).

Local build

pip install build twine
python -m build              # produces dist/*.whl and dist/*.tar.gz
twine check --strict dist/*  # validate PyPI metadata

Release workflow (recommended)

Releases are automated via GitHub Actions using PyPI Trusted Publishing (no long-lived API tokens). One-time setup on PyPI / TestPyPI: add this repo as a trusted publisher for the paylinker project, with workflow publish.yml and environments pypi / testpypi.

Then for each release:

# 1. Bump version in src/paylinker/_version.py
# 2. Update CHANGELOG.md
git commit -am "release: v0.2.0"

# 3. Tag & push  → triggers TestPyPI publish
git tag v0.2.0
git push origin main --tags

# 4. Create a GitHub Release for the tag → triggers real PyPI publish
gh release create v0.2.0 --notes-from-tag

Manual publish (fallback)

python -m build
twine upload --repository testpypi dist/*    # TestPyPI
twine upload dist/*                          # PyPI

Versioning

Follows SemVer: MAJOR.MINOR.PATCH — breaking / feature / fix.

License

MIT

Metadata

Release files for paylinker 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for paylinker 0.1.1
File Size Uploaded Provenance
paylinker-0.1.1.tar.gz 39.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for paylinker 0.1.1
File Interpreter ABI Platform
paylinker-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 71.9 kB

Release files / paylinker-0.1.1.tar.gz

Download URL paylinker-0.1.1.tar.gz
Size 39.4 kB
Tags Source
SHA-256 checksum
How to use checksums
72f4160a06b731e354162af9d570b957b920f21d7afeb439f4f76dd85057fc00
BLAKE2b-256 checksum
How to use checksums
436b5e20529b8ad01d561be13ce228df0057a6da7748fcc0fc5a64e8f7d0e453
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / paylinker-0.1.1-py3-none-any.whl

Download URL paylinker-0.1.1-py3-none-any.whl
Size 32.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d8935f276a77a8b62576e29f289d7c5a06584064d957b99fd711274efd877444
BLAKE2b-256 checksum
How to use checksums
52bfe02ebaac1f3243e17b641f51ef420824ce53a01c74c25514f7a537f219ae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Security for paylinker 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

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