paylinker
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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| paylinker-0.1.1.tar.gz | 39.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.