Skip to main content

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

  1. Install
  2. 5-minute quickstart
  3. How payments work
  4. Settings reference
  5. Checkout
  6. Templates
  7. JSON / API responses
  8. Fulfillment handlers
  9. Payment context
  10. Status polling
  11. Multi-vendor transfers
  12. Admin
  13. Commands
  14. Errors
  15. Security notes
  16. 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

  1. Explicit items=..., or
  2. First matching relation on the instance from ITEM_RELATIONS, or
  3. The instance itself as a single payable item (id / name / price fields -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_id in the webhook is the payment reference, not the Django user pk.
  • django_user_id is 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_KEY and MERCHANT_PIN in 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=False only 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.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-hesabpay 0.1.8
File Size Uploaded
django_hesabpay-0.1.8.tar.gz 32.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-hesabpay 0.1.8
File Interpreter ABI Platform
django_hesabpay-0.1.8-py3-none-any.whl Python 3 none any Details

Total release size: 70.4 kB

Release files / django_hesabpay-0.1.8.tar.gz

Download URL django_hesabpay-0.1.8.tar.gz
Size 32.1 kB
Tags Source
SHA-256 checksum
How to use checksums
e4001df13414b22abf7ba6c88d455ed873546e34406ff8fb7bd7f802011453e8
BLAKE2b-256 checksum
How to use checksums
b4b7b700a6a08d82851805c753fcffeda114801c536b02e28fe84569f4beb0f0
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

Release files / django_hesabpay-0.1.8-py3-none-any.whl

Download URL django_hesabpay-0.1.8-py3-none-any.whl
Size 38.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
170a734cb85382b651310ed13fd095a93b05ed75cbbab5974c0dda41ccabab1e
BLAKE2b-256 checksum
How to use checksums
813adb63d083e1e52e803d4362b349e62ee6e387eb28bd4f36ec2d4eacfc9801
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

Release history Release notifications | RSS feed

0.1.9

2 release files

This release

0.1.8 This release

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page