---
title: Paynet Integration
description: Integrate Paynet into a Python project with PayTechUZ — payment links, the JSON-RPC webhook, password rotation, sandbox services and a Django example.
source: https://pay-tech.uz/en/docs/integrations/paynet
---

# 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

```bash
pip install "paytechuz[django]"
```

## Payment links

```python
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

```python
# 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

```python
# 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)}}
```

```python
# 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:

```python
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](./multiple-kassas).

## Further reading

- [Multiple kassas](./multiple-kassas)
- [Django Integration](./django)
- [PayTechUZ GitHub repository](https://github.com/PayTechUz/paytechuz)
