Skip to main content

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

  1. Add to INSTALLED_APPS:

    INSTALLED_APPS = [
        ...,
        "django_amatopay",
    ]
    
  2. 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
    
  3. Run migrations:

    python manage.py migrate
    
  4. 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
    
  5. (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.
  • raw on 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)

Source distribution for django-amatopay 0.1.0
File Size Uploaded
django_amatopay-0.1.0.tar.gz 14.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-amatopay 0.1.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.1.0 This release

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