Skip to main content

Bayarcash for Django

PyPI version Python versions Django versions License

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 URLhttps://your-app.test/bayarcash/callback
  • Return URLhttps://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

django_bayarcash-1.0.1.tar.gz (24.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_bayarcash-1.0.1-py3-none-any.whl (25.2 kB view details)

Uploaded Python 3

File details

Details for the file django_bayarcash-1.0.1.tar.gz.

File metadata

  • Download URL: django_bayarcash-1.0.1.tar.gz
  • Upload date:
  • Size: 24.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for django_bayarcash-1.0.1.tar.gz
Algorithm Hash digest
SHA256 907310082191d48c9b2f7ef79fe66a6721972766ac9e0c0c6187ad30e065a9a0
MD5 4143a7df9aeb0e7755ef6619eeaa3d90
BLAKE2b-256 02a050f893d260042bb107fd53419be9d96d5136774da8663010c09892592776

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_bayarcash-1.0.1.tar.gz:

Publisher: publish.yml on bayarcash/django

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_bayarcash-1.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_bayarcash-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ff39d4ee6a508a6a877bf69ca3627f545c7300786668c961f9b7c524c99fc60c
MD5 a289402b851a141714e2c66e69b4f203
BLAKE2b-256 115f25051c6ef02b012910d5bdc62bd425b0bd0d168b64d0621c25e58e1fa3ce

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_bayarcash-1.0.1-py3-none-any.whl:

Publisher: publish.yml on bayarcash/django

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 files

1.0.0

2 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