Skip to main content

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]"
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​

MethodBehaviour
GetInformationquotes the account; 302 if it does not exist or is paid already
PerformTransactionpays the account once; 201 for any repeated transactionId, 413 for a wrong amount, 411 without a transactionId
CheckTransactionstate 1 paid, 2 cancelled, 3 unknown
CancelTransaction202 if already cancelled, 203 if unknown
GetStatementpayments in dateFrom…dateTo (YYYY-MM-DD HH:MM:SS), 414 for a bad date
ChangePasswordstores 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.

Further reading​