django-amatopay
Drop-in Django app for AmatoPay: local models mirroring every AmatoPay
object, a full Django admin management UI, a webhook receiver with signature verification, and
Django signals for every event type — all built on top of the
amatopay client library.
Install
uv add django-amatopay
# or
pip install django-amatopay
Setup
-
Add to
INSTALLED_APPS:INSTALLED_APPS = [ ..., "django_amatopay", ]
-
Configure your merchant credentials:
AMATOPAY_API_KEY = env("AMATOPAY_API_KEY") # sk_live_... / sk_test_... AMATOPAY_WEBHOOK_SECRET = env("AMATOPAY_WEBHOOK_SECRET") # whsec_... # AMATOPAY_BASE_URL = "https://api.amatopay.bi/api/v1" # optional, this is the default
-
Run migrations:
python manage.py migrate
-
Mount the webhook receiver and register the resulting URL with AmatoPay:
urlpatterns = [ path("amatopay/", include("django_amatopay.urls")), ] # -> register https://yoursite.com/amatopay/webhook/ with AmatoPay
-
(Optional) backfill existing data:
python manage.py sync_amatopay
That's it — /admin/ now has an "AmatoPay" section listing every checkout session, payment,
delivery, and buyer-protection claim, kept live by incoming webhooks.
What's included
| Piece | What it does |
|---|---|
django_amatopay.services |
Create checkout/QR sessions, verify aliases, quote fees, confirm deliveries, sync any resource — thin wrappers around amatopay that also upsert the local mirror. |
django_amatopay.models |
AmatoCheckoutSession, AmatoPayment (+AmatoPaymentHistory), AmatoDelivery (+AmatoDeliveryConfirmation), AmatoProtectionClaim (+AmatoProtectionClaimEvidence), AmatoWebhookEvent. Every row keeps the full raw API response in .raw so nothing is ever lost to an unmodeled field. |
django_amatopay.admin |
Read-only(ish) ModelAdmin for every model above, with search/filter/date-hierarchy and "re-sync from AmatoPay" actions. |
django_amatopay.views.AmatoPayWebhookView |
Verifies AmatoPay-Signature, de-duplicates by event id, best-effort re-syncs the affected row, and fires the matching Django signal. |
django_amatopay.signals |
One django.dispatch.Signal per AmatoPay event type (payment_paid, payment_failed, delivery_confirmed, settlement_completed, ...). |
manage.py sync_amatopay |
Backfill / periodic refresh of every local mirror table. |
Usage
Take a payment
from django_amatopay import services
session = services.create_checkout_session(
order_number="ORDER-1001",
amount="100000.00",
currency="BIF",
payer_alias="+25779000000",
return_url="https://yoursite.com/orders/1001/",
)
return redirect(session.checkout_url)
session is a saved AmatoCheckoutSession row — session.payment_reference links to the
AmatoPayment created once the payer completes it (kept in sync by the webhook receiver).
React to events
# yourapp/signals.py
from django.dispatch import receiver
from django_amatopay.signals import payment_paid, payment_disputed
@receiver(payment_paid)
def on_payment_paid(sender, event, data, **kwargs):
Order.objects.filter(payment_reference=data["payment_reference"]).update(status="paid")
@receiver(payment_disputed)
def on_payment_disputed(sender, event, data, **kwargs):
notify_ops_team(data["payment_reference"])
Wire it up in your app's AppConfig.ready() as usual.
Manage fulfillment
from django_amatopay import services
services.mark_shipped(delivery.id, tracking_number="DHL-12345")
services.mark_delivered(delivery.id)
services.confirm_delivery("AMP-PAY-...", secure_code="123456")
Or do all of the above from /admin/amatopay/amatodelivery/ directly.
Design notes
- AmatoPay is always the source of truth. Every model is a read-mostly cache; the admin disables free-text editing and instead offers "re-sync from AmatoPay" actions. Don't fork state locally — change it at AmatoPay and re-sync.
rawon every model holds the full, unmodeled API response, so a field this app hasn't wrapped yet (or a future one AmatoPay adds) is never silently dropped.- Webhooks are idempotent. Delivery is de-duplicated by event id before any handler runs, so a retried delivery (AmatoPay retries anything but a 2xx) is a safe no-op, not a duplicate side effect.
Development
uv sync # installs amatopay from ../amatopay-python (see [tool.uv.sources])
uv run pytest # run the test suite (pytest-django + responses, sqlite in-memory)
uv run ruff check . # lint
uv build # build the sdist + wheel into dist/
Once amatopay is published to PyPI, remove the [tool.uv.sources] override in
pyproject.toml so this package depends on the published version instead of the sibling checkout.
License
MIT
Release files for django-amatopay 0.1.0
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_amatopay-0.1.0.tar.gz | 14.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_amatopay-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.6 kB
Release files / django_amatopay-0.1.0.tar.gz
| Download URL | django_amatopay-0.1.0.tar.gz |
|---|---|
| Size | 14.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dba6718482a0b4f5745cfe8b8564905a41e8536b53cb6977b3aea532152e289b
|
|
BLAKE2b-256 checksum How to use checksums |
05e1077432c04213c07ea49529ef2a4b04185055b30e9961ba88a3418dc7a7a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / django_amatopay-0.1.0-py3-none-any.whl
| Download URL | django_amatopay-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1b22ed178d09d6c56367b533ef96781ba2ff7bc7164820fd97a44c9b13af8bb5
|
|
BLAKE2b-256 checksum How to use checksums |
845410f97aac4839b6adf4147533b9f2ab54c2d70c91c0469002d0d8c3f24e5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|