Skip to main content

Accept payments from every major provider in Uzbekistan with a single, beautiful API. Payme, Click, Uzum, Paynet, Octo — sync & async.

Project description

Tolov

Unified payment SDK for Uzbekistan

PyPI version Python License Downloads

Accept payments from every major provider in Uzbekistan with a single, beautiful API.
Payme • Click • Uzum • Paynet • Octo — sync & async — Django & FastAPI ready



Supported Providers

Provider Pay Link API Webhooks
Payme + + +
Click + + +
Uzum + + +
Paynet + - +
Octo + + +

Key Features

  • Sync & async (httpx)
  • Django, FastAPI, Flask integrations
  • Webhook handlers out of the box
  • Automatic transaction tracking
  • Card tokenization (Payme, Click)
  • Receipt management (Payme)
  • Refund API (Octo, Uzum, Click)

Table of Contents


Installation

pip install tolov

With framework extras:

pip install tolov[django]     # Django + DRF
pip install tolov[fastapi]    # FastAPI + SQLAlchemy
pip install tolov[flask]      # Flask + Flask-SQLAlchemy

Quick Start

Payment Links

from tolov import PaymeGateway, ClickGateway, UzumGateway, OctoGateway
from tolov.gateways.paynet.client import PaynetGateway

# --- Payme ---
payme = PaymeGateway(payme_id="ID", payme_key="KEY", is_test_mode=True)

payme_url = payme.create_payment(
    id="order_1",
    amount=150_000,                  # in som
    return_url="https://example.com/done",
    account_field_name="order_id",   # Payme-specific (default: "order_id")
)

# --- Click ---
click = ClickGateway(
    service_id="SID", merchant_id="MID",
    merchant_user_id="MUID", secret_key="SECRET",
)

click_url = click.create_payment(
    id="order_1",
    amount=150_000,
    return_url="https://example.com/done",
)

# --- Uzum ---
uzum = UzumGateway(service_id="498624684")

uzum_url = uzum.create_payment(
    id="order_1",
    amount=100_000,                  # in som, converted to tiyin automatically
    return_url="https://example.com/done",
)

# --- Octo ---
octo = OctoGateway(
    octo_shop_id=123,
    octo_secret="your-secret",
    notify_url="https://example.com/octo/webhook",
)

octo_url = octo.create_payment(
    id="order_1",
    amount=50_000,
    return_url="https://example.com/done",
)

# --- Paynet ---
paynet = PaynetGateway(merchant_id=12345)

paynet_url = paynet.create_payment(
    id="order_1",
    amount=15_000_000,               # in tiyin
)
# Without amount (configured on Paynet side):
paynet_url = paynet.create_payment(id="order_1")

Async Usage

Same class names, same methods — just import from tolov.aio:

from tolov.aio import PaymeGateway, ClickGateway, OctoGateway, UzumGateway

payme = PaymeGateway(payme_id="ID", payme_key="KEY")

# Sync methods (no HTTP) work as-is
url = payme.create_payment(id="order_1", amount=150_000, return_url="...")

# Async methods (HTTP calls) use await
status = await payme.check_payment(transaction_id="receipt_abc")
result = await payme.cancel_payment(transaction_id="receipt_abc")

# Octo — fully async
octo = OctoGateway(octo_shop_id=123, octo_secret="secret", notify_url="...")
url = await octo.create_payment(id="order_1", amount=50_000, return_url="...")
status = await octo.check_payment(transaction_id="shop_tx_123")
refund = await octo.cancel_payment(transaction_id="octo-uuid", amount=50_000)

Receipts & Cards (Payme)

from tolov import PaymeGateway

payme = PaymeGateway(payme_id="ID", payme_key="KEY")

# Cards
card = payme.cards.create(card_number="8600...", expire_date="03/25")
payme.cards.get_verify_code(token=card["result"]["card"]["token"])
payme.cards.verify(token=card["result"]["card"]["token"], code="123456")
payme.cards.check(token="...")
payme.cards.remove(token="...")

# Receipts
receipt = payme.receipts.create(
    amount=500_000,                       # in tiyin
    account={"order_id": "123"},
    description="Payment for order #123",
)
payme.receipts.pay(receipt_id="...", token="card_token")
payme.receipts.check(receipt_id="...")
payme.receipts.cancel(receipt_id="...", reason="Customer request")
payme.receipts.send(receipt_id="...", phone="998901234567")

Async variants — same interface:

from tolov.aio import PaymeGateway

payme = PaymeGateway(payme_id="ID", payme_key="KEY")

card = await payme.cards.create(card_number="8600...", expire_date="03/25")
receipt = await payme.receipts.create(amount=500_000, account={"order_id": "123"})

Card Tokens (Click)

from tolov import ClickGateway

click = ClickGateway(
    service_id="SID", merchant_id="MID",
    merchant_user_id="MUID", secret_key="SECRET",
)

# Request card token
result = click.card_token_request(
    card_number="5614681005030279",
    expire_date="0330",
)

# Verify with SMS code
click.card_token_verify(card_token="token_abc", sms_code="12345")

# Pay using token
click.card_token_payment(
    card_token="token_abc",
    amount=100_000,
    transaction_parameter="unique_tx_id",
)

Octo Payments

from tolov import OctoGateway

octo = OctoGateway(
    octo_shop_id=123,
    octo_secret="your-secret",
    notify_url="https://example.com/octo/webhook",
    is_test_mode=True,
)

# Create payment (one-stage, auto-capture)
url = octo.create_payment(
    id="order_1",
    amount=50_000,
    return_url="https://example.com/done",
    currency="UZS",
    language="uz",
    ttl=15,                             # payment page TTL in minutes
)
# Redirect user to url

# Check status
status = octo.check_payment(transaction_id="order_1")

# Refund
refund = octo.cancel_payment(
    transaction_id="octo-payment-uuid",  # octo_payment_UUID from create response
    amount=50_000,
)

Django Integration

1. Settings

# settings.py
INSTALLED_APPS = [
    # ...
    "tolov.integrations.django",
]

TOLOV = {
    "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",
        "COMMISSION_PERCENT": 0.0,
        "ONE_TIME_PAYMENT": True,
    },
    "UZUM": {
        "SERVICE_ID": "your_service_id",
        "USERNAME": "your_username",
        "PASSWORD": "your_password",
        "ACCOUNT_MODEL": "orders.models.Order",
        "ACCOUNT_FIELD": "order_id",
        "AMOUNT_FIELD": "amount",
        "ONE_TIME_PAYMENT": True,
    },
    "PAYNET": {
        "SERVICE_ID": "your_service_id",
        "USERNAME": "your_username",
        "PASSWORD": "your_password",
        "ACCOUNT_MODEL": "orders.models.Order",
        "ACCOUNT_FIELD": "id",
        "AMOUNT_FIELD": "amount",
        "ONE_TIME_PAYMENT": True,
    },
}

Note: IS_TEST_MODE is set when creating gateway instances (PaymeGateway(is_test_mode=True)), not in webhook settings. Webhooks use the same URL in both environments.

2. Order Model

# models.py
from django.db import models

class Order(models.Model):
    STATUS_CHOICES = [
        ("pending", "Pending"),
        ("paid", "Paid"),
        ("cancelled", "Cancelled"),
    ]

    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(auto_now_add=True)

3. Webhook Handlers

# views.py
from tolov.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
        return {
            "detail": {
                "receipt_type": 0,
                "items": [{
                    "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
        return {
            "fields": {
                "first_name": account.user.first_name,
                "balance": str(account.amount),
            }
        }


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()

4. URLs

# urls.py
from django.urls import path
from .views import (
    PaymeWebhookView, ClickWebhookView, UzumWebhookView,
    PaynetWebhookView, OctoWebhookView,
)

urlpatterns = [
    path("payments/webhook/payme/", PaymeWebhookView.as_view()),
    path("payments/webhook/click/", ClickWebhookView.as_view()),
    path("payments/webhook/uzum/<str:action>/", UzumWebhookView.as_view()),
    path("payments/webhook/paynet/", PaynetWebhookView.as_view()),
    path("payments/webhook/octo/", OctoWebhookView.as_view()),
]

FastAPI Integration

1. Database Setup

from datetime import datetime, timezone
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
from sqlalchemy.orm import sessionmaker, declarative_base

from tolov.integrations.fastapi.models import run_migrations

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


class Order(Base):
    __tablename__ = "orders"

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


# Create payment transaction tables
run_migrations(engine)

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

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

2. Webhook Handlers

from fastapi import FastAPI, Request, Depends
from sqlalchemy.orm import Session
from tolov.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_payme_id",
        payme_key="your_payme_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",
        one_time_payment=True,
    )
    return await handler.handle_webhook(request)

API Reference

Gateway Constructors

Gateway Required Parameters
PaymeGateway payme_id, payme_key
ClickGateway service_id, merchant_id
UzumGateway service_id
PaynetGateway merchant_id
OctoGateway octo_shop_id, octo_secret

All gateways accept is_test_mode=True for sandbox environments.

Unified Interface

Every gateway implements create_payment(), check_payment(), and cancel_payment():

create_payment(id, amount, ...) -> str           # Payment URL
check_payment(transaction_id)   -> dict          # Status + details
cancel_payment(transaction_id)  -> dict          # Cancellation result

Sync vs Async

# Sync
from tolov import PaymeGateway, ClickGateway, OctoGateway, UzumGateway

# Async (same class names, same methods)
from tolov.aio import PaymeGateway, ClickGateway, OctoGateway, UzumGateway

Methods that make HTTP calls become async automatically. Methods that only build URLs (like create_payment for Payme, Click, Uzum, Paynet) remain synchronous even on async gateways.


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

tolov-2.0.1.tar.gz (60.8 kB view details)

Uploaded Source

Built Distribution

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

tolov-2.0.1-py3-none-any.whl (83.3 kB view details)

Uploaded Python 3

File details

Details for the file tolov-2.0.1.tar.gz.

File metadata

  • Download URL: tolov-2.0.1.tar.gz
  • Upload date:
  • Size: 60.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.19

File hashes

Hashes for tolov-2.0.1.tar.gz
Algorithm Hash digest
SHA256 0f59cdf4e02207be0b055026b86bc86747569874955987d4d74433fda7e17624
MD5 2412973b7b4d75c5075a9fed9e50704b
BLAKE2b-256 92d77cde22cd7163ddcff8f7ef2dd7434be5ed19de57a23c660686e8f16efe17

See more details on using hashes here.

File details

Details for the file tolov-2.0.1-py3-none-any.whl.

File metadata

  • Download URL: tolov-2.0.1-py3-none-any.whl
  • Upload date:
  • Size: 83.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.19

File hashes

Hashes for tolov-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5b9b0e9cfb98e2338c0004cd7600292bb38497f962f680a8bc33d8c0cb4a8a5a
MD5 ab5d8336b4b1bc1f7a4f06eb16babc4b
BLAKE2b-256 dacbb55acab894dbf0230b6068c85e3d5978d3e5f8dbd34c27e3eb55f7669039

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