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.0.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.0-py3-none-any.whl (83.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tolov-2.0.0.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.0.tar.gz
Algorithm Hash digest
SHA256 453f6a0cd1b0b48a1f43989551a05b8ffeecc14a263dec3597a5573bfdb392ec
MD5 496c9ab13a439782934b3d68d2d3fafb
BLAKE2b-256 ff1bc5e9431f5dca45f4a09934c12a439603909a6fd6bf42ffdb225237f36d55

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for tolov-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2ebd4eaa45a2b28e05ee186b666e0536bc303ff7a0223cd75a2ae8a9709832ff
MD5 6453a2fa577af6935e292e4ef21853b3
BLAKE2b-256 00e1e471deb2729981ace34fce2a270f9acce608fb1339ab83ffef6257ecb62a

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