Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

paytechuz

PyPI version Python Versions Documentation License: MIT

PayTechUZ is a unified payment library for integrating with popular payment systems in Uzbekistan. It provides a simple and consistent interface for working with Payme, Click, Uzum, Paynet, and Octo payment gateways.

Fully open source: pure Python, no compiled extensions, no license keys, no telemetry. Everything the package does is in this repository.

📖 Documentation | 💬 Telegram

Features

  • Unified API: consistent interface for multiple payment providers
  • Framework integration: native support for Django and FastAPI
  • Webhook handling: ready-to-use webhook handlers for payment notifications
  • Transaction management: automatic transaction tracking in a single table
  • Extensible: adding a new provider means adding one gateway and one webhook class

Installation

pip install paytechuz

# For Django
pip install "paytechuz[django]"

# For FastAPI
pip install "paytechuz[fastapi]"

Quick Start

Generate payment links

from paytechuz.gateways.payme import PaymeGateway
from paytechuz.gateways.click import ClickGateway
from paytechuz.gateways.uzum import UzumGateway
from paytechuz.gateways.paynet import PaynetGateway
from paytechuz.gateways.octo import OctoGateway

# Payme
payme = PaymeGateway(
    payme_id="your_payme_id",
    payme_key="your_payme_key",
    is_test_mode=True,  # Set to False in production
)

# Click
click = ClickGateway(
    service_id="your_service_id",
    merchant_id="your_merchant_id",
    merchant_user_id="your_merchant_user_id",
    secret_key="your_secret_key",
    is_test_mode=True,
)

# Uzum (Biller / open-service)
uzum = UzumGateway(
    service_id="your_service_id",
    is_test_mode=True,
)

# Paynet
paynet = PaynetGateway(
    merchant_id="your_merchant_id",  # accepts both str and int
    is_test_mode=False,
)

# Octo
octo = OctoGateway(
    octo_shop_id=123,
    octo_secret="your_octo_secret",
    notify_url="https://example.com/payments/webhook/octo/",
    is_test_mode=True,
)
# Payme — amount in som. account_field_name is the field name used in the
# payment URL (e.g. ac.id=123). Default: "order_id".
payme_link = payme.create_payment(
    id="order_123",
    amount=150000,
    return_url="https://example.com/return",
    account_field_name="id",
)

# Click — amount in som
click_link = click.create_payment(
    id="order_123",
    amount=150000,
    description="Test payment",
    return_url="https://example.com/return",
)

# Uzum — amount in som, converted to tiyin in the URL
uzum_link = uzum.create_payment(
    id="order_123",
    amount=100000,
    return_url="https://example.com/callback",
)
# https://www.uzumbank.uz/open-service?serviceId=...&order_id=order_123&amount=10000000&redirectUrl=...

# Paynet — amount in tiyin, optional
paynet_link = paynet.create_payment(id="order_123", amount=15000000)
# https://app.paynet.uz/?m=your_merchant_id&c=order_123&a=15000000

paynet_link_no_amount = paynet.create_payment(id="order_123")
# https://app.paynet.uz/?m=your_merchant_id&c=order_123

# Octo — amount in som, one-stage (auto_capture)
octo_link = octo.create_payment(
    id="order_123",
    amount=50000,
    return_url="https://example.com/payment/done/",
    description="Order #123",
)

You can also build a gateway by name:

from paytechuz import create_gateway

payme = create_gateway("payme", payme_id="...", payme_key="...", is_test_mode=True)

Provider notes

Payme account_field_name — only Payme uses this parameter. It sets the account key inside the payment URL (ac.<account_field_name>=<id>). Other gateways ignore it.

Paynet — URL-based, mobile-first:

  • URL format: https://app.paynet.uz/?m={merchant_id}&c={payment_id}&a={amount}
  • merchant_id accepts both str and int
  • amount is optional and expressed in tiyin
  • no return URL support
  • desktop users scan a QR code, mobile users open the Paynet app
  • payment status arrives through JSON-RPC 2.0 webhooks

Django Integration

  1. Create your order model:
# models.py
from django.db import models
from django.utils import timezone


class Order(models.Model):
    STATUS_CHOICES = (
        ('pending', 'Pending'),
        ('paid', 'Paid'),
        ('cancelled', 'Cancelled'),
        ('delivered', 'Delivered'),
    )

    product_name = models.CharField(max_length=255)
    amount = models.DecimalField(max_digits=12, decimal_places=2)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
    created_at = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return f"{self.id} - {self.product_name} ({self.amount})"
  1. Add the app and configure the settings:
# settings.py
INSTALLED_APPS = [
    # ...
    'paytechuz.integrations.django',
]

PAYTECHUZ = {
    'PAYME': {
        'PAYME_ID': 'your_payme_id',
        'PAYME_KEY': 'your_payme_key',
        'ACCOUNT_MODEL': 'orders.models.Order',
        'ACCOUNT_FIELD': 'id',
        'AMOUNT_FIELD': 'amount',
        'ONE_TIME_PAYMENT': True,
    },
    'CLICK': {
        'SERVICE_ID': 'your_service_id',
        'MERCHANT_ID': 'your_merchant_id',
        'MERCHANT_USER_ID': 'your_merchant_user_id',
        'SECRET_KEY': 'your_secret_key',
        'ACCOUNT_MODEL': 'orders.models.Order',
        'ACCOUNT_FIELD': 'id',
        'AMOUNT_FIELD': 'amount',
        'COMMISSION_PERCENT': 0.0,
        'ONE_TIME_PAYMENT': True,
    },
    'UZUM': {
        'SERVICE_ID': 'your_service_id',
        'USERNAME': 'your_uzum_username',   # webhook Basic auth
        'PASSWORD': 'your_uzum_password',   # webhook Basic auth
        'ACCOUNT_MODEL': 'orders.models.Order',
        'ACCOUNT_FIELD': 'order_id',        # or 'id'
        'AMOUNT_FIELD': 'amount',
        'ONE_TIME_PAYMENT': True,
    },
    'PAYNET': {
        'SERVICE_ID': 'your_paynet_service_id',
        'USERNAME': 'your_paynet_username',
        'PASSWORD': 'your_paynet_password',
        'ACCOUNT_MODEL': 'orders.models.Order',
        'ACCOUNT_FIELD': 'id',
        'AMOUNT_FIELD': 'amount',
        'ONE_TIME_PAYMENT': True,
    },
    'OCTO_BANK': {
        'OCTO_SHOP_ID': 42125,
        'OCTO_SECRET': 'your_octo_secret',
        'OCTO_UNIQUE_KEY': 'your_octo_unique_key',  # required in production
        'NOTIFY_URL': 'https://example.com/payments/webhook/octo/',
        'ACCOUNT_MODEL': 'orders.models.Order',
        'ACCOUNT_FIELD': 'id',
        'AMOUNT_FIELD': 'amount',
        'ONE_TIME_PAYMENT': True,
        'TEST_MODE': True,  # False in production — enables signature verification
    },
}

Note: is_test_mode is a gateway constructor argument, not a webhook setting. Webhooks receive requests on the same URL in both test and production.

  1. Run migrations:
python manage.py migrate
  1. Create the webhook views:
# views.py
from paytechuz.integrations.django.views import (
    BasePaymeWebhookView,
    BaseClickWebhookView,
    BaseUzumWebhookView,
    BasePaynetWebhookView,
    BaseOctoWebhookView,
)
from .models import Order


class PaymeWebhookView(BasePaymeWebhookView):
    def successfully_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'paid'
        order.save()

    def cancelled_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'cancelled'
        order.save()

    def get_check_data(self, params, account):  # optional
        # Extra data for CheckPerformTransaction (fiscal receipt)
        return {
            "additional": {"first_name": account.first_name, "balance": account.balance},
            "detail": {
                "receipt_type": 0,
                "shipping": {"title": "Yetkazib berish", "price": 10000},
                "items": [
                    {
                        "discount": 0,
                        "title": account.product_name,
                        "price": int(account.amount * 100),
                        "count": 1,
                        "code": "00001",
                        "units": 1,
                        "vat_percent": 0,
                        "package_code": "123456",
                    }
                ],
            },
        }


class ClickWebhookView(BaseClickWebhookView):
    def successfully_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'paid'
        order.save()

    def cancelled_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'cancelled'
        order.save()


class UzumWebhookView(BaseUzumWebhookView):
    def successfully_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'paid'
        order.save()

    def cancelled_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'cancelled'
        order.save()

    def get_check_data(self, params, account):  # optional
        return {"fio": {"value": "Ivanov Ivan"}}


class PaynetWebhookView(BasePaynetWebhookView):
    def successfully_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'paid'
        order.save()

    def cancelled_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'cancelled'
        order.save()

    def get_check_data(self, params, account):  # optional
        # Extra data for GetInformation
        return {
            "fields": {
                "first_name": account.user.first_name,
                "balance": account.user.balance,
            }
        }


class OctoWebhookView(BaseOctoWebhookView):
    def successfully_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'paid'
        order.save()

    def cancelled_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'cancelled'
        order.save()
  1. Register the webhook URLs:
# urls.py
from django.urls import path

from .views import (
    PaymeWebhookView,
    ClickWebhookView,
    UzumWebhookView,
    PaynetWebhookView,
    OctoWebhookView,
)

urlpatterns = [
    path('payments/webhook/payme/', PaymeWebhookView.as_view(), name='payme_webhook'),
    path('payments/webhook/click/', ClickWebhookView.as_view(), name='click_webhook'),
    path('payments/webhook/uzum/<str:action>/', UzumWebhookView.as_view(), name='uzum_webhook'),
    path('payments/webhook/paynet/', PaynetWebhookView.as_view(), name='paynet_webhook'),
    path('payments/webhook/octo/', OctoWebhookView.as_view(), name='octo_webhook'),
]

FastAPI Integration

  1. Set up the database models:
from datetime import datetime, timezone

from sqlalchemy import create_engine, Column, Integer, String, Numeric, DateTime
from sqlalchemy.orm import declarative_base, sessionmaker

from paytechuz.integrations.fastapi import run_migrations

SQLALCHEMY_DATABASE_URL = "sqlite:///./payments.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)

Base = declarative_base()


class Order(Base):
    __tablename__ = "orders"

    id = Column(Integer, primary_key=True, index=True)
    product_name = Column(String, index=True)
    amount = Column(Numeric(12, 2))
    status = Column(String, default="pending")
    created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))


# Create the paytechuz payments table
run_migrations(engine)

# Create your own tables
Base.metadata.create_all(bind=engine)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
  1. Create the webhook routes:
from fastapi import FastAPI, Request, Depends
from sqlalchemy.orm import Session

from paytechuz.integrations.fastapi import PaymeWebhookHandler, ClickWebhookHandler

app = FastAPI()


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


class CustomPaymeWebhookHandler(PaymeWebhookHandler):
    def successfully_payment(self, params, transaction):
        order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
        order.status = "paid"
        self.db.commit()

    def cancelled_payment(self, params, transaction):
        order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
        order.status = "cancelled"
        self.db.commit()


class CustomClickWebhookHandler(ClickWebhookHandler):
    def successfully_payment(self, params, transaction):
        order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
        order.status = "paid"
        self.db.commit()

    def cancelled_payment(self, params, transaction):
        order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
        order.status = "cancelled"
        self.db.commit()


@app.post("/payments/payme/webhook")
async def payme_webhook(request: Request, db: Session = Depends(get_db)):
    handler = CustomPaymeWebhookHandler(
        db=db,
        payme_id="your_merchant_id",
        payme_key="your_merchant_key",
        account_model=Order,
        account_field='id',
        amount_field='amount',
    )
    return await handler.handle_webhook(request)


@app.post("/payments/click/webhook")
async def click_webhook(request: Request, db: Session = Depends(get_db)):
    handler = CustomClickWebhookHandler(
        db=db,
        service_id="your_service_id",
        secret_key="your_secret_key",
        account_model=Order,
        account_field='id',
        amount_field='amount',
        one_time_payment=True,
    )
    return await handler.handle_webhook(request)

The FastAPI integration currently ships webhook handlers for Payme and Click. Uzum, Paynet and Octo webhooks are available for Django.

Project layout

paytechuz/
├── core/                     # shared base classes, HTTP client, exceptions, utils
├── gateways/                 # one package per provider (client + constants)
│   ├── payme/  click/  uzum/  paynet/  octo/
└── integrations/
    ├── django/               # models, admin, signals, migrations
    │   ├── webhooks/         # one webhook handler per provider
    │   └── views.py          # CSRF-exempt base views
    └── fastapi/              # SQLAlchemy models, pydantic schemas, handlers

docs/abstract_webhook.md describes the shared webhook contract and the checklist to follow when adding a new provider.

Development

make install-dev   # editable install with django, fastapi and dev extras
make test          # run the test suite
make lint          # flake8
make build         # build sdist + wheel into dist/

License

MIT — see LICENSE.

Release files for paytechuz 0.4.0b1

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

Source distribution (sdist)

Source distribution for paytechuz 0.4.0b1
File Size Uploaded
paytechuz-0.4.0b1.tar.gz 57.9 kB Details

Built distribution (wheel)

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

Total release size: 130.6 kB

Release files / paytechuz-0.4.0b1.tar.gz

Download URL paytechuz-0.4.0b1.tar.gz
Size 57.9 kB
Tags Source
SHA-256 checksum
How to use checksums
0bbe7db34213ed818110121fdd9fabb5ad22366e829878d1c6cc66a3fa9c9c67
BLAKE2b-256 checksum
How to use checksums
fbcaf4e4f0ec02fd3cec3773f7b54c167cef21e2a0b4080fc30a600f0c367286
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / paytechuz-0.4.0b1-py3-none-any.whl

Download URL paytechuz-0.4.0b1-py3-none-any.whl
Size 72.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1a2f5b2f39b8a218879c192a8e73e75a43c5f94b1df8cd1ce129f93cfd9dddb5
BLAKE2b-256 checksum
How to use checksums
edddb005db3ee59236a40eea8b6b3f16eab3f48f8ab92effb97385fac32957e7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6
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