Bayarcash for Django
A Django integration for the Bayarcash payment gateway. It
wraps the framework-agnostic bayarcash SDK
and adds a Django-idiomatic developer experience: settings config, a payable model
mixin, optional database persistence, checksum-verified callback/return views,
scheduled reconciliation, and signals.
Targets Bayarcash API v3.
It fits two setups:
- Single merchant — one set of credentials in settings. Everything in Usage works out of the box.
- Multi-tenant (SaaS) — each tenant has its own Bayarcash account with credentials stored in your database. See Multi-tenant.
Either way you choose whether to store payment records in your database with
STORE_RECORDS.
Requirements
- Python 3.8+
- Django 3.2, 4.x, or 5.x
Installation
pip install django-bayarcash
Add the app to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"django.contrib.contenttypes",
"django_bayarcash",
]
Run the migrations (skip if you want stateless mode):
python manage.py migrate django_bayarcash
The per-tenant bayarcash_accounts table is a separate, opt-in migration
(0002). Single-merchant projects can stop at 0001:
python manage.py migrate django_bayarcash 0001
Configuration
Add a BAYARCASH dict to your settings:
BAYARCASH = {
"TOKEN": "your-personal-access-token",
"SECRET_KEY": "your-api-secret-key",
"SANDBOX": True,
# Optional (defaults shown)
"TIMEOUT": 30,
"STORE_RECORDS": True,
"CALLBACK": {"enabled": True, "path": "bayarcash/callback"},
"RETURN": {"enabled": True, "path": "bayarcash/return", "redirect": None},
"RECONCILE": {"enabled": True, "requery_after": 2, "cancel_after": 60},
# "CREDENTIAL_RESOLVER": "django_bayarcash.credentials.DatabaseCredentialResolver",
# "MULTI_TENANT": False,
# "ENCRYPTION_KEY": None, # falls back to Django SECRET_KEY
}
Include the webhook routes:
# urls.py
from django.urls import include, path
urlpatterns = [
# ...
path("", include("django_bayarcash.urls")),
]
In the Bayarcash portal, point your portal's URLs at these routes:
- Callback URL →
https://your-app.test/bayarcash/callback - Return URL →
https://your-app.test/bayarcash/return
Usage
1. Make a model payable
Add the BayarcashPayableMixin to any model (an Order, User, Invoice, ...):
from django.db import models
from django_bayarcash.mixins import BayarcashPayableMixin
class Order(BayarcashPayableMixin):
reference = models.CharField(max_length=64)
This adds payments and mandates relations plus the charge() and
enroll_direct_debit() helpers. (Prefer not to inherit? Use the standalone
django_bayarcash.mixins.charge(owner, data, tenant=None) helper.)
2. Create a payment
from bayarcash import Bayarcash
from django.shortcuts import redirect
intent = order.charge({
"portal_key": "your_portal_key",
"payment_channel": Bayarcash.FPX,
"order_number": order.reference, # optional; auto-generated when omitted
"amount": "10.00",
"payer_name": order.customer_name,
"payer_email": order.customer_email,
"payer_telephone_number": order.customer_phone,
})
# Redirect the customer to the hosted checkout.
return redirect(intent.url)
The checksum is generated for you. When record storage is enabled, a pending
BayarcashTransaction is stored and linked to order, and a payment_created
signal fires.
3. Enrol a Direct Debit mandate
from bayarcash import FpxDirectDebit
mandate = order.enroll_direct_debit({
"portal_key": "your_portal_key",
"amount": "10.00",
"payer_name": "Ahmad bin Abdullah",
"payer_id_type": FpxDirectDebit.NRIC,
"payer_id": "900101011234",
"payer_email": "ahmad@example.com",
"payer_telephone_number": "0123456789",
"application_reason": "Monthly subscription",
"frequency_mode": FpxDirectDebit.MODE_MONTHLY,
})
return redirect(mandate.url)
4. Handle results
The package registers two views automatically — you do not write them:
| Route | Method | Purpose |
|---|---|---|
/bayarcash/callback |
POST |
Server-to-server, authoritative. Checksum-verified (invalid → 403). Updates the transaction and fires the status signal. |
/bayarcash/return |
GET |
Browser redirect, best-effort. Verifies the checksum when present, never aborts, then redirects (or returns JSON). |
Set RETURN["redirect"] to a URL or URL name to control where the customer lands
after payment. When it is None, the return route responds with JSON.
5. Listen for signals
from django.dispatch import receiver
from django_bayarcash.signals import payment_succeeded
@receiver(payment_succeeded)
def on_paid(sender, transaction, **kwargs):
transaction.owner.mark_paid()
Available signals:
| Signal | Payload kwargs |
|---|---|
payment_created |
transaction |
payment_succeeded |
transaction |
payment_failed |
transaction |
payment_cancelled |
transaction |
mandate_authorized |
mandate |
mandate_approved |
mandate |
webhook_received |
record_type, payload |
6. Query stored records
from django_bayarcash.models import BayarcashTransaction
order.payments.all()
BayarcashTransaction.objects.successful()
BayarcashTransaction.objects.pending()
transaction.status_label() # "Successful", "Pending", ...
Reconciliation
Callbacks and return redirects can be missed (downtime, network issues). The
package ships a bayarcash_reconcile command that re-queries pending payments and
auto-cancels stale ones:
python manage.py bayarcash_reconcile
Django has no built-in scheduler, so run it on a schedule. With cron (every minute):
* * * * * cd /path/to/project && /path/to/venv/bin/python manage.py bayarcash_reconcile >> /dev/null 2>&1
Or with Celery beat:
# celery.py
app.conf.beat_schedule = {
"bayarcash-reconcile": {
"task": "django_bayarcash.reconcile", # a thin task that calls call_command
"schedule": 60.0,
},
}
from celery import shared_task
from django.core.management import call_command
@shared_task(name="django_bayarcash.reconcile")
def reconcile():
call_command("bayarcash_reconcile")
Reconciliation requires stored records.
Store records (store data, or pass-through)
STORE_RECORDS decides whether the package keeps a local copy of every payment and
mandate in your database.
STORE_RECORDS = True (default) — stateful
Transactions and mandates are recorded in the bayarcash_transactions and
bayarcash_mandates tables. This is what you get:
| Capability | What happens |
|---|---|
Pending row on charge() |
A BayarcashTransaction is created, linked to the payable model (order.payments), storing the payment_intent_id so the callback can complete the same row. |
| Automatic webhook writes | Callbacks update the record's status, set paid_at on success, and store the verified payload in raw_callback. |
| Queryable history | BayarcashTransaction.objects.successful(), .pending(), status labels, reporting — no extra API calls. |
| Reconciliation | bayarcash_reconcile can re-query and auto-cancel stale pending payments. |
STORE_RECORDS = False — stateless (pass-through)
The package becomes a thin SDK wrapper. charge() / enroll_direct_debit() create
the intent and return it without touching the database, and the callback/return
views still verify checksums and fire signals — they just skip persistence. No
migrations are needed, and bayarcash_reconcile is disabled. Use this when you
already store payment state yourself and only want checksum-safe request/callback
handling.
Multi-tenant (credentials in the database)
In a SaaS app each tenant has its own Bayarcash account. Store each tenant's credentials in your own table and let the package resolve them per request. There is one shared webhook for every tenant — no tenant id in the URL.
1. Store per-tenant credentials
The package ships an encrypted bayarcash_accounts table for this. Apply its
opt-in migration:
python manage.py migrate django_bayarcash
Then store each tenant's credentials — token and secret_key are encrypted at rest:
from django_bayarcash.models import BayarcashAccount
BayarcashAccount.objects.create(
tenant_id=tenant.id,
token=token,
secret_key=secret_key,
sandbox=False,
)
2. Turn multi-tenant on
Point the package at its built-in resolver and enable multi-tenant:
BAYARCASH = {
# ...
"CREDENTIAL_RESOLVER": "django_bayarcash.credentials.DatabaseCredentialResolver",
"MULTI_TENANT": True,
}
Credentials already elsewhere? If your tenants' Bayarcash credentials live on
your own model/table, skip the migration and implement the resolver yourself —
return token, secret_key, and sandbox for a tenant:
from django_bayarcash.credentials import CredentialResolver
class MyCredentialResolver(CredentialResolver):
def resolve(self, tenant=None):
account = MyAccount.objects.get(tenant_id=tenant)
return {
"token": account.token,
"secret_key": account.secret_key,
"sandbox": bool(account.sandbox),
}
3. Create payments per tenant
Pass the tenant to charge() / enroll_direct_debit(). The package generates the
checksum and calls the gateway with that tenant's credentials, and stamps the
stored row with tenant_id:
intent = order.charge(data, tenant=tenant_id)
With no tenant argument the default settings credentials are used — so single-
and multi-tenant code live side by side.
4. One shared webhook for every tenant
Point every tenant's portal Callback/Return URLs at the same package routes.
The package matches each callback to its local record, resolves that tenant's
secret, and verifies the checksum — rejecting with 403 (fail closed) when no
record matches. This lookup is why multi-tenant mode requires STORE_RECORDS.
The client
For direct, lower-level access to the SDK:
from django_bayarcash.manager import get_client
client = get_client() # default credentials, pinned to API v3
client = get_client(tenant="t1") # a specific tenant's credentials
portals = client.get_portals()
intent = client.get_payment_intent("payment_intent_id")
Error handling
SDK calls raise typed exceptions you can catch around charge() /
enroll_direct_debit():
from bayarcash.exceptions import BayarcashError, ValidationError
try:
intent = order.charge({...})
except ValidationError as exc:
errors = exc.errors # 422
except BayarcashError as exc:
...
Testing
pip install -e ".[dev]"
pytest
License
The MIT License (MIT).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_bayarcash-1.0.0.tar.gz.
File metadata
- Download URL: django_bayarcash-1.0.0.tar.gz
- Upload date:
- Size: 23.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4cd675d678ef1c9f6bf24e075bf0e9d270f64b3cf9cb6abcf025cf16e83bb416
|
|
| MD5 |
488b54bb0bc51e6c0588dc8c55cbe17f
|
|
| BLAKE2b-256 |
0b1c0af8ca43f4a4b25da1567c6aa3332fd6ecc56a0a60918a9f75e995eaf632
|
Provenance
The following attestation bundles were made for django_bayarcash-1.0.0.tar.gz:
Publisher:
publish.yml on bayarcash/django
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_bayarcash-1.0.0.tar.gz -
Subject digest:
4cd675d678ef1c9f6bf24e075bf0e9d270f64b3cf9cb6abcf025cf16e83bb416 - Sigstore transparency entry: 2215861712
- Sigstore integration time:
-
Permalink:
bayarcash/django@42a00ee3fcf3aedc406eb22eb0e44f30ac99f459 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/bayarcash
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@42a00ee3fcf3aedc406eb22eb0e44f30ac99f459 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_bayarcash-1.0.0-py3-none-any.whl.
File metadata
- Download URL: django_bayarcash-1.0.0-py3-none-any.whl
- Upload date:
- Size: 25.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2b77c185e81a65186cec75fa0a1fd386b7fb42d805b99c99ddeb31c19168670
|
|
| MD5 |
c64d819fc6394a77ad8f36dfbeb8d3ac
|
|
| BLAKE2b-256 |
7883813881e3429a6f7f084275d5fd68f9bd61b433bd653824a76b4be2ba7ad5
|
Provenance
The following attestation bundles were made for django_bayarcash-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on bayarcash/django
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_bayarcash-1.0.0-py3-none-any.whl -
Subject digest:
a2b77c185e81a65186cec75fa0a1fd386b7fb42d805b99c99ddeb31c19168670 - Sigstore transparency entry: 2215861733
- Sigstore integration time:
-
Permalink:
bayarcash/django@42a00ee3fcf3aedc406eb22eb0e44f30ac99f459 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/bayarcash
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@42a00ee3fcf3aedc406eb22eb0e44f30ac99f459 -
Trigger Event:
push
-
Statement type: