Paynet Integration
Paynet takes payments in its app and at its terminals. It calls your server over JSON-RPC 2.0 to look up an account, take the payment and check it later. This guide covers both halves with PayTechUZ.
Installation
pip install "paytechuz[django]"
Payment links
from paytechuz.gateways.paynet.client import PaynetGateway
paynet = PaynetGateway(merchant_id="your_merchant_id")
# Amount in tiyin, optional
link = paynet.create_payment(id="order_123", amount=15000000)
# https://app.paynet.uz/?m=your_merchant_id&c=order_123&a=15000000
The link has no return URL. Desktop visitors scan a QR code, phones open the Paynet app.
Django webhook
Settings
# settings.py
INSTALLED_APPS = [
# ...
'paytechuz.integrations.django',
]
PAYTECHUZ = {
'PAYNET': {
'SERVICE_ID': 'your_service_id',
'USERNAME': 'your_paynet_login',
'PASSWORD': 'your_paynet_password',
'ACCOUNT_MODEL': 'shop.models.Order',
'ACCOUNT_FIELD': 'order_id', # the field Paynet sends in params.fields
'AMOUNT_FIELD': 'amount', # in so'm on your model; Paynet sends tiyin
'ONE_TIME_PAYMENT': True,
},
}
Run python manage.py migrate. It creates the payments table and
paynet_service_credentials, where the password Paynet rotates is kept.
View
# views.py
from paytechuz.integrations.django.views import BasePaynetWebhookView
from shop.models import Order
class PaynetWebhookView(BasePaynetWebhookView):
def successfully_payment(self, params, transaction):
Order.objects.filter(id=transaction.account_id).update(status='paid')
def cancelled_payment(self, params, transaction):
Order.objects.filter(id=transaction.account_id).update(status='cancelled')
def get_check_data(self, params, account): # optional: GetInformation
return {'fields': {'amount': int(account.amount)}}
# urls.py
path('payments/webhook/paynet/', PaynetWebhookView.as_view(), name='paynet_webhook'),
successfully_payment runs in the same database transaction as the payment
row. If it raises, nothing is recorded and Paynet hears 102, so the
customer is not charged for a payment you did not take.
What the webhook answers
| Method | Behaviour |
|---|---|
GetInformation | quotes the account; 302 if it does not exist or is paid already |
PerformTransaction | pays the account once; 201 for any repeated transactionId, 413 for a wrong amount, 411 without a transactionId |
CheckTransaction | state 1 paid, 2 cancelled, 3 unknown |
CancelTransaction | 202 if already cancelled, 203 if unknown |
GetStatement | payments in dateFrom…dateTo (YYYY-MM-DD HH:MM:SS), 414 for a bad date |
ChangePassword | stores the new password, at least 8 characters |
A wrong login answers 412 with HTTP 401, before anything about the body is
read. An empty login or password in your settings refuses every call.
Password rotation
Paynet changes the webhook password with ChangePassword. PayTechUZ stores
it in PaynetCredential (or the model named by CREDENTIAL_MODEL) and checks
calls against it from then on. Your PASSWORD setting applies only until the
first rotation, so a redeploy does not bring the old password back.
Sandbox
Mark a service as a sandbox while Paynet's tester runs through it:
PAYTECHUZ = {
'PAYNET': {
'KASSAS': {
'test': {'SERVICE_ID': '1003', 'USERNAME': '...', 'PASSWORD': '...',
'IS_SANDBOX': True},
},
# ACCOUNT_MODEL etc. as above
},
}
Every call is logged; payments are recorded but sandbox_payment(params, transaction) is called instead of successfully_payment. A paid account
still answers 302, exactly as it will live, and sandbox rows stay out of the
live statement once the service goes live.
Several Paynet services on one URL are covered in Multiple kassas.