# Ultraner with FastAPI

Declaring a Pydantic model on the webhook route is the natural thing to do and it destroys the bytes you need to verify.

- Language: Python
- Client: ultraner (https://github.com/Ultraner/ultraner-python)
- Human version of this page: https://ultraner.com/frameworks/fastapi
- 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

Take `Request` and `await request.body()` for the webhook. A typed body model parses and re-serialises, so the signature can never match.

## Install

```bash
pip install ultraner
```

## Charge a mobile money wallet

```
# main.py
import os, uuid
from fastapi import FastAPI
from ultraner import Ultraner

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

@app.post("/pay")
async def pay(phone: str, amount: int):
    payment = ultraner.payments.create_mno_charge(
        amount=amount,             # whole shillings: TZS has no minor unit
        currency="TZS",
        provider="Vodacom",        # provider code, not brand name
        account_number=phone,
        idempotency_key=str(uuid.uuid4()),
    )
    return {"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.

```
# main.py
import hmac, hashlib, json, os
from fastapi import Request, Response

@app.post("/webhooks/ultraner")
async def ultraner_webhook(request: Request):
    # Request, not a Pydantic model: a model would parse and re-serialise,
    # and the signature is over the bytes as sent.
    raw = await request.body()
    signature = request.headers.get("X-Ultraner-Signature", "")

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

    if not hmac.compare_digest(signature, expected):
        return Response(status_code=400)

    event = json.loads(raw)
    background.add_task(handle, event)   # acknowledge first, work after
    return Response(status_code=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
