Metadata-Version: 2.4
Name: namibrapay
Version: 0.1.0
Summary: Official NamibraPay SDK for Python — collections, payouts, verification and webhooks.
Author-email: NamibraPay <developers@namibrapay.com>
License: MIT
Project-URL: Homepage, https://docs.namibrapay.com
Project-URL: Repository, https://github.com/Namibra-Devs/NamibraPay-SDK_v2
Project-URL: Issues, https://github.com/Namibra-Devs/NamibraPay-SDK_v2/issues
Keywords: namibrapay,payments,mobile-money,ghana,fintech,api,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# namibrapay (Python)

Official NamibraPay client for Python. Collections, payouts, verification,
invoices and status — with zero dependencies.

- Standard library only (`urllib`, `hmac`, `json`)
- Sync client, Python 3.9+
- PEP 561 typed (`py.typed` included)
- Retries that never double-charge

## Install

```bash
pip install namibrapay
```

## Quick start

```python
import os
from namibrapay import NamibraPay

client = NamibraPay(
    api_key=os.environ["NAMBRAPAY_API_KEY"],
    api_secret=os.environ["NAMBRAPAY_API_SECRET"],
)

tx = client.transactions.collect(
    amount=2500,  # GHS 25.00, in minor units
    channel="mtn_momo",
    payer_identifier="0244000000",
    reference="ORDER-1042",
)

print(tx["reference"], tx["status"])
```

Omit the credentials to read `NAMBRAPAY_API_KEY` and `NAMBRAPAY_API_SECRET`
from the environment:

```python
client = NamibraPay()
```

Python arguments are `snake_case`; the SDK maps them to the API's `camelCase`
fields for you (`payer_identifier` → `payerIdentifier`).

## Configuration

| Option | Default | Notes |
| --- | --- | --- |
| `api_key` / `api_secret` | env vars | Sent as `X-API-Key` / `X-API-Secret` |
| `base_url` | `https://api.namibrapay.com/api/v1` | Must include the version segment |
| `timeout` | `30.0` | Per attempt, in seconds |
| `max_retries` | `2` | Applies only to retry-safe calls |
| `headers` | `None` | Extra headers on every request |
| `transport` | `urllib` | Advanced/testing hook |

## Resources

```python
client.transactions.collect(amount=..., channel=..., payer_identifier=..., reference=..., idempotency_key=None)
client.transactions.initialize(amount=..., reference=...)
client.transactions.payout(amount=..., channel=..., recipient_account=..., recipient_bank=..., recipient_name=..., reference=...)
client.transactions.get(reference)
client.transactions.receipt(reference)
client.transactions.reverse(transaction_id, reason=..., amount=None)

client.checkout.retrieve(reference)
client.checkout.pay(reference, channel=..., payer_identifier=...)

client.payment_links.retrieve(slug)
client.payment_links.charge(slug, channel=..., payer_identifier=..., amount=None)

client.invoices.retrieve(code)
client.invoices.pay(code)

client.virtual_accounts.create(customer_name=..., customer_phone=..., ghana_card_number=...)
client.virtual_accounts.list()
client.virtual_accounts.sync_balance(account_id)

client.verification.banks()
client.verification.networks()
client.verification.resolve_account(bank_reference=..., account_number=..., amount=None)
client.verification.resolve_wallet(network=..., wallet_number=...)
client.verification.verify_id(id_type="ghanacard", id_number=...)
client.verification.confirm_payee(payee_type=..., expected_name=..., account_number=..., bank_reference=None, network=None)

client.status.snapshot()
client.status.incidents(page=None, limit=None)   # {"data": [...], "pagination": {...}}
client.status.feed()                             # RSS 2.0 as a string

client.health()
```

Every method returns the `data` object from the envelope directly — a `dict`,
not a custom model, so new API fields arrive without an SDK release.

## Money amounts

Amounts are integers in minor units (pesewas) and must be whole cedis:
`GHS 10.00` → `1000`. Collections have a minimum of `100`, payouts `1000`. The
SDK raises `NamibraPayError` before any request is made when the value is
invalid.

## Errors

```python
from namibrapay import NamibraPayAPIError, NamibraPayConnectionError, NamibraPayError

try:
    client.transactions.payout(...)
except NamibraPayAPIError as error:
    if error.code == "BIZ_009":
        ...  # insufficient balance, etc.
    print(error.status_code, error.errors)
except NamibraPayConnectionError as error:
    print("network failed", error.cause)
```

`NamibraPayAPIError` exposes `status_code`, `code`, `errors`, `body`,
`retry_after_seconds` and `is_retryable`.

## Retries and idempotency

The SDK retries `408`, `429`, `500`, `502`, `503` and `504` with backoff and
honours `Retry-After`.

| Call | Retried? | Why |
| --- | --- | --- |
| GET (`get`, `receipt`, `status`, …) | Yes | Reads are safe to repeat |
| `collect`, `initialize`, `payout` | Yes | The API deduplicates on `reference` |
| Any call with `idempotency_key` | Yes | The key makes the retry safe |
| `reverse`, `checkout.pay`, `payment_links.charge`, `invoices.pay`, `virtual_accounts.create` | No | A retry would create a second money movement |

## Webhooks

```python
from flask import Flask, request, abort
from namibrapay.webhooks import construct_webhook_event, WebhookSignatureError

app = Flask(__name__)

@app.post("/webhooks/namibrapay")
def receive():
    try:
        event = construct_webhook_event(
            request.get_data(),  # raw bytes, not request.json
            request.headers.get("X-Webhook-Signature", ""),
            app.config["NAMBRAPAY_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError:
        abort(401)

    # Acknowledge fast; do the work out of band.
    return "", 200
```

`verify_webhook_signature` and `compute_webhook_signature` are available if you
prefer to handle the details yourself. Comparison is constant-time.

## Async

This release is synchronous. Inside async frameworks (FastAPI, Starlette, aiohttp)
run calls in a worker thread so the event loop stays free:

```python
from starlette.concurrency import run_in_threadpool

tx = await run_in_threadpool(client.transactions.get, "ORDER-1042")
```

## Versioning

The package follows semver. Additions are minor, breaking changes are major.
`namibrapay.__version__` matches `pyproject.toml`.

## License

[MIT](../../LICENSE)
