# Ultraner with Django

Django parses and CSRF-protects POST bodies by default, both of which break a webhook endpoint before your view runs.

- Language: Python
- Client: ultraner (https://github.com/Ultraner/ultraner-python)
- Human version of this page: https://ultraner.com/frameworks/django
- Live in 14 markets: Benin, Cameroon, DR Congo, Gabon, Ivory Coast, Kenya, Mozambique, Republic of Congo, Rwanda, Senegal, Sierra Leone, Tanzania, Uganda and Zambia

## The mistake this framework invites

The webhook view needs `@csrf_exempt` and must read `request.body`, not `request.POST`. A third party cannot hold your CSRF token, so the protection only blocks the delivery.

## Install

```bash
pip install ultraner
```

## Charge a mobile money wallet

```
# views.py
import os, uuid
from django.http import JsonResponse
from ultraner import Ultraner

ultraner = Ultraner(os.environ["ULTRANER_API_KEY"])

def pay(request):
    payment = ultraner.payments.create_mno_charge(
        amount=5000,               # whole shillings: TZS has no minor unit
        currency="TZS",
        provider="Vodacom",        # provider code, not brand name
        account_number="255700000000",
        idempotency_key=str(uuid.uuid4()),
    )
    # Accepted, not paid. The payer still has to approve on their handset.
    return JsonResponse({"reference": payment.reference, "status": payment.status})
```

## Verify a webhook

Where framework differences actually bite: the signature is computed over
the raw body, so any middleware that parses JSON before you read it will
break verification.

```
# views.py
import hmac, hashlib, json, os
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse

@csrf_exempt                       # a third party cannot hold your CSRF token
def ultraner_webhook(request):
    raw = request.body             # bytes, exactly as delivered
    signature = request.headers.get("X-Ultraner-Signature", "")

    expected = hmac.new(
        os.environ["ULTRANER_WEBHOOK_SECRET"].encode(),
        raw,
        hashlib.sha256,
    ).hexdigest()

    # compare_digest, not ==, so timing does not leak the signature.
    if not hmac.compare_digest(signature, expected):
        return HttpResponse(status=400)

    event = json.loads(raw)
    # Acknowledge fast; retries will redeliver the same event id.
    handle.delay(event)            # Celery, or any queue
    return HttpResponse(status=200)
```

## Things that are true regardless of framework

- **Amounts are whole units in most African currencies.** 5000 TZS is five
  thousand shillings, not fifty. Read `decimals` from
  https://ultraner.com/ai/context/currencies.json rather than assuming cents.
- **A charge is asynchronous.** A 200 means the prompt reached the payer's
  handset, not that they paid. Poll
  `GET /v1/payments/express/status/{transaction_id}` or wait for the webhook.
- **A charge takes `provider`; a payout takes `network`.** Both take
  `account_number`, never `accountNumber`. Unknown keys are stripped
  rather than rejected, so a wrong name does not error: the recipient
  disappears and the call fails as a missing required field.
- **Paying money out needs `X-Signature-Key` and `Idempotency-Key`.**

## More

- Every framework: https://ultraner.com/frameworks
- API reference: https://ultraner.com/docs
- The whole reference as Markdown: https://ultraner.com/docs.md
- OpenAPI: https://ultraner.com/openapi.json
- Agent skills: https://github.com/Ultraner/ultraner-skills
- Machine-readable index of all of this: https://ultraner.com/ai/context/frameworks.json
