Accept payments from every major provider in Uzbekistan with a single, beautiful API. Payme, Click, Uzum, Paynet, Octo, Multicard — sync & async.
Project description
Tolov
Unified payment SDK for Uzbekistan
Accept payments from every major provider in Uzbekistan with a single, beautiful API.
Payme • Click • Uzum • Paynet • Octo • Multicard — sync & async — Django & FastAPI ready
One MulticardGateway integration → accept 18+ payment methods on a single checkout
Payme |
Click |
Uzum |
Alif |
Apex |
Davr |
Alliance |
Rahmat |
Asterium |
Paynet |
Anor |
Xazna |
Beepul |
Oson |
Trast |
OFB |
Morpara |
СБП |
|
Supported Providers
|
Key Features
|
Table of Contents
Installation
pip install tolov
With framework extras:
pip install tolov[django] # Django + DRF
pip install tolov[fastapi] # FastAPI + 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,
)
Multicard
Multicard uses token-based auth (the SDK fetches and refreshes the JWT for you)
and a single store_id. The top-level create_payment opens an invoice
(payment page) and returns its checkout_url.
Multicard is an aggregator — one
MulticardGatewayintegration accepts every method shown at the top of this README (cards, wallets, and banks) on a single checkout page, with no separate per-provider setup.
from tolov import MulticardGateway
mc = MulticardGateway(
application_id="your_application_id",
secret="your_secret",
store_id=123,
is_test_mode=True,
)
# Create an invoice — returns checkout_url (amount in som)
url = mc.create_payment(
id="order_1",
amount=150_000,
return_url="https://example.com/done",
callback_url="https://example.com/payments/webhook/multicard",
)
# Check status (by Multicard transaction uuid)
status = mc.check_payment(transaction_id="<uuid>") # -> {"status", "state", "data"}
# Refund
mc.cancel_payment(transaction_id="<uuid>")
# Lower-level sub-clients (amounts in tiyin):
mc.invoices.create(amount=15_000_000, invoice_id="order_1", callback_url="...")
mc.invoices.get("<uuid>")
mc.invoices.delete("<uuid>") # annul an unpaid invoice
mc.payments.info("<uuid>")
mc.payments.refund("<uuid>")
Async — same names, await the HTTP calls:
from tolov.aio import MulticardGateway
mc = MulticardGateway(application_id="...", secret="...", store_id=123)
url = await mc.create_payment(id="order_1", amount=150_000, callback_url="...")
status = await mc.check_payment(transaction_id="<uuid>")
Card binding (form). Redirect the user to form_url; once they finish,
read the resulting card_token either from the (unsigned) bind callback or by
polling check_binding(session_id). Store the token yourself — tolov does not
persist cards.
res = mc.cards.bind(
redirect_url="https://example.com/cards/ok",
redirect_decline_url="https://example.com/cards/fail",
callback_url="https://example.com/cards/callback",
phone="998901234567",
)
session_id, form_url = res["session_id"], res["form_url"] # redirect user to form_url
binding = mc.cards.check_binding(session_id) # -> card_token, card_pan, status, ...
mc.cards.info_by_token("<card_token>")
mc.cards.check_pinfl(pan="8600...", pinfl="12345678901234") # Uzcard/Humo only
mc.cards.revoke_token("<card_token>")
Token payments, refunds & fiscal (amounts in tiyin):
# Charge a saved card token (optional split + OFD fiscal data)
payment = mc.payments.create_by_token(
card_token="<card_token>",
amount=500_000,
invoice_id="order_1",
split=[{"type": "card", "amount": 100_000, "details": "partner share",
"recipient": "<bank-details-uuid>"}],
)
# If payment["otp_hash"] is not null, an SMS code is required:
mc.payments.confirm(payment["uuid"], otp="123456")
# Pay via an external app (payme/click/uzum/...) — returns checkout_url/deeplink
app = mc.payments.app_pay(payment_system="payme", amount=500_000, invoice_id="order_2")
mc.payments.info("<uuid>")
mc.payments.refund("<uuid>") # full refund
mc.payments.partial_refund("<uuid>", refund_amount=20_000, ofd=[...])
mc.payments.send_fiscal("<uuid>", url="https://ofd.example/check/...")
Holds (block funds, then debit or cancel; expiry in minutes, ≤ 30 days):
hold = mc.holds.create(card_token="<card_token>", amount=500_000,
invoice_id="order_1", expiry=60)
hold_id = hold["id"]
mc.holds.confirm(hold_id, otp="123456") # block the funds
mc.holds.debit(hold_id, amount=300_000) # capture (partial allowed)
# or release before capture:
mc.holds.cancel(hold_id)
mc.holds.info(hold_id)
Payouts (credit a card by pan or saved token; kyc_data required > 10M som):
payout = mc.payouts.create(amount=10_000, invoice_id="po_1", token="<card_token>")
# If created with confirmable=True, confirm with an OTP:
mc.payouts.confirm(payout["uuid"], otp="123456")
mc.payouts.info("<uuid>")
Reporting (read-only; dates YYYY-mm-dd HH:MM:SS, GMT+5):
mc.reports.app_info() # wallet balance, OTP settings, ...
mc.reports.recipient_details("<merchant-account-uuid>")
mc.reports.payment_registry("2026-06-01 00:00:00", "2026-06-26 23:59:59", limit=100)
mc.reports.payout_history("2026-06-01 00:00:00", "2026-06-26 23:59:59")
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,
},
"MULTICARD": {
"APPLICATION_ID": "your_application_id",
"SECRET": "your_secret", # also signs the success callback
"STORE_ID": 123,
# "CALLBACK_SECRET": "...", # optional; defaults to SECRET
"ACCOUNT_MODEL": "orders.models.Order",
"ACCOUNT_FIELD": "id",
},
}
Note:
IS_TEST_MODEis 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,
BaseMulticardWebhookView,
)
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()
class MulticardWebhookView(BaseMulticardWebhookView):
# Multicard's success callback fires only on a successful payment;
# the signature is verified for you before this runs.
def successfully_payment(self, params, transaction):
order = Order.objects.get(id=transaction.account_id)
order.status = "paid"
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()),
path("payments/webhook/multicard/", MulticardWebhookView.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,
MulticardWebhookHandler,
)
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)
class CustomMulticardWebhookHandler(MulticardWebhookHandler):
def successfully_payment(self, params, transaction):
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
order.status = "paid"
self.db.commit()
@app.post("/payments/multicard/webhook")
async def multicard_webhook(request: Request, db: Session = Depends(get_db)):
handler = CustomMulticardWebhookHandler(
db=db,
secret="your_secret", # the secret that signs the callback
account_model=Order,
account_field="id",
)
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 |
MulticardGateway |
application_id, secret, store_id |
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, MulticardGateway
# Async (same class names, same methods)
from tolov.aio import PaymeGateway, ClickGateway, OctoGateway, UzumGateway, MulticardGateway
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.
Development
uv sync # install the dev environment
uv run pytest # full suite (live tests skip if the sandbox is unreachable)
uv run pytest -m "not live" # offline only (respx-mocked)
uv run pytest -m live # live Multicard sandbox integration tests
Release notes live in CHANGELOG.md.
License
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tolov-2.1.0.tar.gz.
File metadata
- Download URL: tolov-2.1.0.tar.gz
- Upload date:
- Size: 78.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a17657b5e8bed51dcdaa8e7b439efe49304674b618355a191a303d5003e8a2f
|
|
| MD5 |
59506129453ae3f6a0ae99c5490da881
|
|
| BLAKE2b-256 |
2748690a1054d505ab96b39f4b3fcb88c851fa143789d3f22d9489f3ba606856
|
File details
Details for the file tolov-2.1.0-py3-none-any.whl.
File metadata
- Download URL: tolov-2.1.0-py3-none-any.whl
- Upload date:
- Size: 102.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f06d763afa4de42fb0312f38f5002960bce660e68066c4850016b4e5b378999
|
|
| MD5 |
a459347344e123441bb33c5859ab365f
|
|
| BLAKE2b-256 |
7fe428475de85e9c5e981806658f3553bf43091a18399c230ca82218b06088bd
|