django-hesabpay
Django SDK for the HesabPay Payment Gateway.
Hosted checkout in a few lines. Your app owns fulfillment. HesabPay webhooks are the source of truth -redirects are UX only.
import hesabpay
from hesabpay import on_payment_success
checkout = hesabpay.checkout(order, user=request.user)
return redirect(checkout.url)
@on_payment_success
def fulfill(context):
enroll(user=context.user, course=context.instance)
- PyPI: django-hesabpay
- HesabPay docs: docs.hesab.com
- Requires: Python 3.10+, Django 4.2+
- Currency: AFN (no FX conversion in v1)(will be added later)
Table of contents
- Install
- 5-minute quickstart
- How payments work
- Settings reference
- Checkout
- Templates
- JSON / API responses
- Fulfillment handlers
- Payment context
- Status polling
- Multi-vendor transfers
- Admin
- Commands
- Errors
- Security notes
- Changelog
Install
pip install django-hesabpay
Editable (from this repo):
pip install -e .
5-minute quickstart
1. Add the app
# settings.py
INSTALLED_APPS = [
# ...
"hesabpay",
]
2. Configure
HESABPAY = {
"ENVIRONMENT": "sandbox", # or "production"
"SANDBOX_API_KEY": "your-sandbox-api-key",
# "PRODUCTION_API_KEY": "your-live-api-key",
"PUBLIC_BASE_URL": "https://your-public-https-origin.example",
"SUCCESS_URL": "/payments/success/",
"FAILURE_URL": "/payments/failure/",
"HANDLERS": ["shop.payments"], # import path for @on_payment_success modules
}
PUBLIC_BASE_URL turns relative success/failure paths into absolute HTTPS URLs HesabPay can redirect to (required behind tunnels / reverse proxies). SITE_URL is accepted as an alias.
3. Migrate
python manage.py migrate
Webhook and status routes register automatically:
| Endpoint | Purpose |
|---|---|
POST /hesabpay/webhook/ |
HesabPay → your app (truth) |
GET /hesabpay/status/<reference>/ |
Poll local payment status |
Copy the webhook URL into the HesabPay dashboard (or sandbox equivalent).
4. Start checkout
# views.py
import hesabpay
from django.shortcuts import redirect
def pay(request, order_id):
order = Order.objects.get(pk=order_id)
session = hesabpay.checkout(order, user=request.user)
return redirect(session.url)
5. Fulfill after a verified webhook
# shop/payments.py ← listed in HESABPAY["HANDLERS"]
from hesabpay import on_payment_success
@on_payment_success
def fulfill(context):
if not context.is_first_success:
return # idempotent retries / replays
order = context.instance
order.mark_paid()
send_receipt(context.user, order)
Sanity-check config anytime:
python manage.py hesabpay doctor
How payments work
checkout(instance)
→ local HesabPayPayment (reference = HesabPay user_id)
→ POST /api/v1/payment/create-session
→ redirect customer to hosted checkout
customer pays on HesabPay
→ browser redirect to SUCCESS_URL / FAILURE_URL (UX only)
→ HesabPay POST webhook
→ verify-signature
→ resolve payment by user_id / reference
→ mark payment succeeded|failed
→ on_commit → @on_payment_success / @on_payment_failed
Rules of thumb
| Do | Don't |
|---|---|
Fulfill in @on_payment_success |
Fulfill only on the success redirect |
| Treat webhooks as truth | Trust query-string redirect payloads alone |
| Keep API keys + PIN server-side | Put secrets in frontend / templates |
If a merchant handler crashes after acknowledgment, the payment stays succeeded. Fix the bug and use Replay fulfillment handlers in admin -never roll back a verified payment because fulfillment failed.
Settings reference
All keys live under HESABPAY = { ... }.
Required / common
| Key | Default | Description |
|---|---|---|
ENVIRONMENT |
"sandbox" |
"sandbox" or "production" |
SANDBOX_API_KEY |
"" |
Used when environment is sandbox |
PRODUCTION_API_KEY |
"" |
Used when environment is production |
PUBLIC_BASE_URL |
"" |
Public HTTPS origin for redirects (SITE_URL alias OK) |
SUCCESS_URL |
None |
Absolute URL, path, or Django route name |
FAILURE_URL |
None |
Absolute URL, path, or Django route name |
HANDLERS |
[] |
Modules to import so decorators register |
LOG_LEVEL |
"INFO" |
DEBUG, INFO, WARNING, ERROR, or CRITICAL for HesabPay logs |
Webhooks & URLs
| Key | Default |
|---|---|
WEBHOOK_ENABLED |
True |
AUTO_REGISTER_URLS |
True |
WEBHOOK_PATH |
"/hesabpay/webhook/" |
STATUS_PATH |
"/hesabpay/status/<str:reference>/" |
Items (create-session line items)
| Key | Default |
|---|---|
ITEM_ID_FIELD |
"id" |
ITEM_NAME_FIELD |
"name" |
ITEM_PRICE_FIELD |
"price" |
ITEM_RELATIONS |
("items", "lines", "orderitem_set") |
Multi-vendor
| Key | Default | Description |
|---|---|---|
MERCHANT_PIN |
"" |
Plain account PIN (encrypted on the wire -see Encrypt PIN) |
AUTO_VENDOR_TRANSFER |
False |
Run @vendor_resolver after first success |
VENDOR_RESOLVER |
None |
Optional dotted callable (prefer @vendor_resolver) |
Optional object updates
| Key | Default |
|---|---|
AUTO_UPDATE_STATUS |
False |
STATUS_FIELD |
"status" |
PAID_VALUE |
"paid" |
FAILED_VALUE |
"failed" |
HTTP
| Key | Default |
|---|---|
TIMEOUT_SECONDS |
30.0 |
API bases (fixed by environment):
- Sandbox:
https://api-sandbox.hesab.com - Production:
https://api.hesab.com
Checkout
import hesabpay
session = hesabpay.checkout(
order, # any model instance (GFK stored locally)
user=request.user, # optional; stored on the payment
email="buyer@example.com", # optional; falls back to user.email
items=order.lines.all(), # optional; else ITEM_RELATIONS / instance itself
success_url="/ok/", # optional override
failure_url="/fail/",
request=request, # helps build absolute URLs when needed
metadata={"order_number": "A-100"},
auto_vendor_transfer=False, # optional per-payment override (see Multi-vendor)
)
session.url # hosted checkout URL
session.payment_id # local reference (sent to HesabPay as user_id)
session.status # pending | ...
session.as_dict() # JSON-friendly payload
session.payment # HesabPayPayment row
What gets sent to HesabPay
Only documented create-session fields: user_id (payment reference), items[{id,name,price}], optional email, redirect_success_url, redirect_failure_url.
Redirect URLs get ?hesabpay_payment=<reference> appended so your success page can poll status.
Resolving line items
- Explicit
items=..., or - First matching relation on the instance from
ITEM_RELATIONS, or - The instance itself as a single payable item (
id/name/pricefields -configurable)
# Single product
session = hesabpay.checkout(product, user=request.user)
# Order with lines
session = hesabpay.checkout(order, user=request.user, items=order.lines.all())
Optional model mixin
from hesabpay.models import HesabPayPayableMixin
class Order(HesabPayPayableMixin, models.Model):
...
session = order.create_hesabpay_checkout(user=request.user)
Templates
{% load hesabpay %}
{# Creates a session immediately and renders a link #}
{% hesabpay_checkout_button order user=request.user %}
{% hesabpay_checkout_button order user=request.user label="Pay now" css_class="btn btn-primary" %}
For more control, call hesabpay.checkout(...) in the view and pass session.url into the template.
JSON / API responses
from django.http import JsonResponse
import hesabpay
def api_checkout(request, order):
session = hesabpay.checkout(order, user=request.user)
return JsonResponse(session.as_dict())
Example body:
{
"payment_id": "hp_…",
"checkout_url": "https://…",
"status": "pending",
"auto_vendor_transfer": null,
"items": [
{"id": "12", "name": "Course access", "price": 250.0}
]
}
auto_vendor_transfer is true / false when you set it on checkout; otherwise null (follow global setting).
Fulfillment handlers
Register handler modules in settings so Django imports them at startup:
HESABPAY = {
# ...
"HANDLERS": ["shop.payments", "billing.hesabpay_handlers"],
}
# shop/payments.py
from hesabpay import on_payment_success, on_payment_failed
@on_payment_success
def on_ok(context):
if not context.is_first_success:
return
# enroll / unlock / email / ship / …
@on_payment_failed
def on_fail(context):
notify_ops(context.payment_id, context.raw_payload)
Handlers run after the payment row is updated, inside transaction.on_commit. Handler failures are logged; the payment stays acknowledged.
Django signals are also available for advanced wiring:
payment_succeeded, payment_failed, webhook_verified, payment_unmatched, vendor_transfer_succeeded, vendor_transfer_failed.
Payment context
@on_payment_success / @on_payment_failed receive a HesabPayPaymentContext:
| Attribute | Meaning |
|---|---|
payment |
Local HesabPayPayment |
event |
Stored webhook event (if any) |
payload |
Parsed HesabPay webhook fields |
instance |
Your GFK object (order, product, …) |
user |
Django user on the payment (if any) |
items |
Local HesabPayPaymentItem rows |
item_instances |
Resolved real item objects when possible |
webhook_items |
items[] from the HesabPay webhook payload |
raw_payload |
Full webhook JSON |
transaction_id |
HesabPay transaction id |
amount |
Decimal amount |
succeeded |
bool |
is_first_success |
True only on the first successful processing |
@on_payment_success
def fulfill(context):
data = context.as_dict()
# payment_id, status, transaction_id, amount, currency, email,
# succeeded, is_first_success, user_id, django_user_id,
# sender_account, memo, transaction_date,
# instance_type, instance_id, items, webhook_items, item_count
Notes:
- HesabPay
user_idin the webhook is the payment reference, not the Django user pk. django_user_idis the auth user id when one was attached at checkout.
Status polling
Useful right after the browser redirect (webhook may arrive slightly later):
GET /hesabpay/status/hp_your_reference/
{
"payment_id": "hp_…",
"status": "succeeded",
"transaction_id": "…",
"amount": "250.00",
"currency": "AFN",
"items": [{"id": "12", "name": "Course access", "price": 250.0}]
}
Disable auto URL registration and include manually if you prefer:
# urls.py
path("payments/", include("hesabpay.urls")),
HESABPAY = {"AUTO_REGISTER_URLS": False, ...}
Multi-vendor transfers
After a customer pays once, you may split funds to vendor accounts via
send-money-MultiVendor with an
encrypted PIN.
Store the plain PIN in settings. The SDK encrypts it (AES-CBC, API-key-derived key) on every request -never send a plaintext PIN on the wire.
HESABPAY = {
# ...
"MERCHANT_PIN": "1234",
"AUTO_VENDOR_TRANSFER": False, # recommended default
}
Manual (recommended)
from hesabpay import on_payment_success, transfer_to_vendors
@on_payment_success
def settle(context):
if not context.is_first_success:
return
transfer_to_vendors(
payment=context.payment,
vendors=[
{"account_number": "700000001", "amount": 50},
{"account_number": "700000002", "amount": 15},
# dozens more -auto-batched
],
)
transfer_to_vendors(...):
- merges duplicate
account_numbers (sums amounts) - splits into batches of 15 (HesabPay max is 16; one slot headroom)
- returns
list[HesabPayVendorPayout](one row per batch) - is idempotent per payment + vendor chunk fingerprint
Override batch size if needed: transfer_to_vendors(..., batch_size=15).
Helper: transfer_to_vendors_from_context(context) uses @vendor_resolver / settings resolver to collect vendors, then transfers.
Automatic via @vendor_resolver
HESABPAY = {
# ...
"MERCHANT_PIN": "1234",
"AUTO_VENDOR_TRANSFER": True,
"HANDLERS": ["shop.payments"],
}
from hesabpay import vendor_resolver
@vendor_resolver
def vendors_for(context):
order = context.instance
return [
{"account_number": line.vendor_account, "amount": float(line.price_afn)}
for line in order.lines.all()
if line.vendor_account
]
Returning [] skips auto settlement for that payment.
Per-checkout opt-out / opt-in
Large apps often auto-settle most orders but handle some manually:
# Skip auto transfer even when AUTO_VENDOR_TRANSFER is True
session = hesabpay.checkout(order, user=request.user, auto_vendor_transfer=False)
# Force auto transfer for this payment even when the global setting is False
session = hesabpay.checkout(order, user=request.user, auto_vendor_transfer=True)
The flag is stored on payment.metadata["auto_vendor_transfer"] and wins over the global setting.
from hesabpay import payment_allows_auto_vendor_transfer
if not payment_allows_auto_vendor_transfer(payment):
transfer_to_vendors(payment=payment, vendors=manual_list)
Admin
Open Django admin → HesabPay:
- Payments -status badges, item snapshots, activity timeline, setup checklist (env, API key present, webhook path)
- Webhook events -verification / processing state; action Replay fulfillment handlers
- Vendor payouts -batch records; action Retry failed vendor transfers
- Logs -human-readable timeline (secrets masked)
Commands
# Config + webhook path + API base
python manage.py hesabpay doctor
# Locally fire a fake verified webhook for a payment reference
python manage.py hesabpay simulate-webhook <payment_reference>
python manage.py hesabpay simulate-webhook <payment_reference> --failure
Errors
Friendly exceptions from hesabpay.exceptions (often include a Hint: …):
| Exception | When |
|---|---|
HesabPayConfigurationError |
Missing API key / PIN / bad config |
HesabPayAuthenticationError |
API rejected credentials |
HesabPayValidationError |
No items, unpaid payout, bad input |
HesabPayAPIError |
Upstream HTTP / API failure |
HesabPayPayoutError |
Multi-vendor transfer failed |
HesabPayError |
Base class |
from hesabpay.exceptions import HesabPayError
try:
session = hesabpay.checkout(order, user=request.user)
except HesabPayError as exc:
logger.exception("checkout failed: %s", exc)
System checks: hesabpay.W001 (missing API key), W002 (production without production key), W003 (bad HANDLERS import).
Security notes
- Keep
SANDBOX_API_KEY/PRODUCTION_API_KEYandMERCHANT_PINin environment / secret store -never in git or the browser. - PIN is stored plain in settings and encrypted per request with your API key (docs). Pre-encrypting the PIN with the same API key does not meaningfully harden a settings leak.
- Webhook signature verification is mandatory (
POST /api/v1/hesab/webhooks/verify-signature). - Set
WEBHOOK_ENABLED=Falseonly for local experiments -the webhook view returns 503 when disabled.
Public API surface
import hesabpay
from hesabpay import (
checkout, # also hesabpay.checkout
on_payment_success,
on_payment_failed,
vendor_resolver,
transfer_to_vendors,
transfer_to_vendors_from_context,
payment_allows_auto_vendor_transfer,
HesabPayPaymentContext,
DEFAULT_VENDOR_BATCH_SIZE, # 15
)
Changelog
See CHANGELOG.md.
License
MIT
Metadata
Release files for django-hesabpay 0.1.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_hesabpay-0.1.9.tar.gz | 32.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_hesabpay-0.1.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.9 kB
Release files / django_hesabpay-0.1.9.tar.gz
| Download URL | django_hesabpay-0.1.9.tar.gz |
|---|---|
| Size | 32.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
15a344374da1d822b30ee7d2b48aaa8af8e6a0115e4adf5ae67ec8bc33828158
|
|
BLAKE2b-256 checksum How to use checksums |
1841949112b229f0f4d17c3aeacb5d8295b5c94e30c6682dd883872c8bef1127
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.
Transparency logRelease files / django_hesabpay-0.1.9-py3-none-any.whl
| Download URL | django_hesabpay-0.1.9-py3-none-any.whl |
|---|---|
| Size | 38.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
77d4ec508baad926984fc29461292df549283706cb5b51f1aa87ee58353493f3
|
|
BLAKE2b-256 checksum How to use checksums |
37595bd44e5ad62cd03677feff1be6dff71db8c3c3fe5d262dde902ca0af8841
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.
Transparency log