Metadata-Version: 2.4
Name: paynexus-gateway
Version: 1.0.0
Summary: Official PayNexus Gateway Python SDK — Accept M-Pesa payments, manage webhooks, and query merchants via the PayNexus API
Author-email: PayNexus <support@paynexus.co.ke>
License: MIT
Project-URL: Homepage, https://www.paynexus.co.ke
Project-URL: Repository, https://github.com/PAYNEXUS-SOLUTIONS/paynexus
Project-URL: Issues, https://github.com/PAYNEXUS-SOLUTIONS/paynexus/issues
Keywords: paynexus-gateway,paynexus,payment,mpesa,stk-push,payment-gateway,kenya,mobile-money,m-pesa,safaricom
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.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: respx>=0.20; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Dynamic: license-file

# PayNexus Gateway Python SDK

Official Python SDK for the [PayNexus Gateway](https://www.paynexus.co.ke) payment platform. Accept M-Pesa STK Push payments, manage webhooks, invoices, payment links, and query merchant data from any Python application.

## Installation

```bash
pip install paynexus-gateway
```

Requires Python 3.9 or later.

## Setup

Get your API keys from the [PayNexus dashboard](https://www.paynexus.co.ke).

### 1. Create a `.env` file

```bash
# .env
PAYNEXUS_SECRET_KEY=sk_live_your_secret_key
PAYNEXUS_WEBHOOK_SECRET=whsec_your_webhook_secret   # Only needed if using webhooks
```

### 2. Load environment variables

```bash
pip install python-dotenv
```

```python
import os
from dotenv import load_dotenv
from paynexus_gateway import PayNexus

load_dotenv()

client = PayNexus(
    secret_key=os.environ["PAYNEXUS_SECRET_KEY"],
    webhook_secret=os.environ.get("PAYNEXUS_WEBHOOK_SECRET"),
)
```

> **Django users:** Add the env vars to your `settings.py` or use `django-environ`.
> **FastAPI users:** Use `pydantic-settings` or `python-dotenv`.

---

## Quick Start — Accept a Payment

```python
from paynexus_gateway import PayNexus, InitiatePaymentParams

client = PayNexus(secret_key="sk_live_...")

payment = client.payments.initiate(
    InitiatePaymentParams(
        payment_account_id=1,      # Your M-Pesa payment account ID
        amount=500,                # Amount in KES (1–150,000)
        phone="254712345678",      # Customer phone number
        description="Order #1234",
    )
)

print(payment["data"])
# {
#   "payment_id": 42,
#   "reference": "PAY-abc123",
#   "checkout_request_id": "ws_CO_...",
#   "status": "pending",
#   "customer_message": "Success. Request accepted for processing",
#   ...
# }
```

---

## API Reference

### Payments

```python
from paynexus_gateway import InitiatePaymentParams, ListPaymentsParams

# Initiate STK Push
result = client.payments.initiate(
    InitiatePaymentParams(
        payment_account_id=1,
        amount=500,
        phone="254712345678",
        description="Order payment",
    )
)

# Check payment status by reference
by_ref = client.payments.get_by_reference("PAY-abc123")

# Check payment status by ID
by_id = client.payments.get_by_id(42)

# Check payment status by M-Pesa CheckoutRequestID
by_checkout = client.payments.get_by_checkout_id("ws_CO_123456")

# List payments (with optional filters)
payments = client.payments.list(
    ListPaymentsParams(
        status="completed",
        payment_method="mpesa",
        from_date="2025-01-01",
        to_date="2025-12-31",
        per_page=50,
    )
)
```

### M-Pesa (Simplified)

The M-Pesa resource provides convenience methods that auto-normalize phone numbers and use your default payment account.

```python
# Validate a phone number
result = client.mpesa.validate_phone("0746990866")
# Returns: {"phone": "254746990866", "valid": true}

# Initiate STK Push (auto-normalizes phone, auto-selects default account)
payment = client.mpesa.initiate_payment(
    amount=100,
    phone="0746990866",  # Will be normalized to 254746990866
    description="Order #12345"
)
# Returns: {"reference": "PAY-...", "checkout_request_id": "ws_CO_...", ...}

# Check payment status by CheckoutRequestID
status = client.mpesa.get_payment_status("ws_CO_123456")
```

### Invoices

```python
# Create an invoice
invoice = client.invoices.create({
    "amount": 1000,
    "description": "Consulting services - June 2026",
    "due_date": "2026-07-01",
    "customer_name": "John Doe",
    "customer_email": "john@example.com",
    "customer_phone": "254712345678",
})

# List invoices
invoices = client.invoices.list({"status": "pending"})

# Get a specific invoice
invoice = client.invoices.get("INV-2026-001")

# Update an invoice
client.invoices.update("INV-2026-001", {"status": "paid"})

# Send invoice to customer
client.invoices.send("INV-2026-001")

# Delete an invoice
client.invoices.delete("INV-2026-001")
```

### Checkout / Payment Links

Create shareable payment links without writing server code.

```python
# Create a payment link session
session = client.checkout.create_session({
    "amount": 100,
    "description": "Premium Access - AI Course",
    "reference": "INV-2026-001",
    "return_url": "https://yoursite.com/thanks",
    "cancel_url": "https://yoursite.com/cancelled",
})

# session["data"] contains:
# {
#   "session_id": "cs_live_abc123...",
#   "checkout_url": "https://paynexus.co.ke/checkout/s/cs_live_abc123...",
#   "expires_at": "2026-06-21T21:25:00+03:00"
# }
```

### Health Checks

```python
# Basic API health
health = client.health.check()

# M-Pesa service status
mpesa_health = client.health.mpesa()
```

### Sandbox

Use sandbox API keys (prefix `sb_`) for testing.

```python
# Create a sandbox client
sandbox_client = PayNexus(
    secret_key="sb_your_sandbox_key",
    base_url="https://paynexus.co.ke/api"
)

# Get sandbox merchant info
merchant = sandbox_client.sandbox.get_merchant()

# Initiate sandbox payment (max 50 KES)
payment = sandbox_client.sandbox.initiate_payment({
    "amount": 10,
    "phone": "254746990866",
    "description": "Test payment"
})

# Check sandbox payment status
status = sandbox_client.sandbox.get_payment_status("SANDBOX-123")
```

### Webhooks

```python
from paynexus_gateway import RegisterWebhookParams, UpdateWebhookParams

# Register a new webhook
hook = client.webhooks.register(
    RegisterWebhookParams(
        name="Payment alerts",
        url="https://example.com/webhooks/paynexus",
        events=["payment.completed", "payment.failed"],
    )
)
# hook["data"]["secret"] → save this, it is only shown once

# Update a webhook
client.webhooks.update(1, UpdateWebhookParams(
    events=["payment.completed", "payment.failed", "invoice.paid"],
))

# List all webhooks
hooks = client.webhooks.list()

# Delete a webhook
client.webhooks.delete(1)
```

**Available webhook events:**

| Event | Description |
|---|---|
| `payment.completed` | Payment succeeded |
| `payment.failed` | Payment failed |
| `payment.initiated` | STK Push sent to customer |
| `invoice.created` | Invoice created |
| `invoice.paid` | Invoice paid |
| `invoice.overdue` | Invoice overdue |
| `account.created` | Account created |
| `account.updated` | Account updated |
| `subscription.created` | Subscription created |
| `subscription.canceled` | Subscription canceled |

### Merchant

```python
# Get your merchant info
me = client.merchant.get()

# List your businesses
businesses = client.merchant.businesses()

# List your M-Pesa payment accounts
accounts = client.merchant.payment_accounts()
```

### API Keys

```python
from paynexus_gateway import CreateApiKeyParams, UpdateApiKeyParams

# Create a new API key
key = client.api_keys.create(
    CreateApiKeyParams(
        name="Production key",
        payment_account_id=1,
        permissions=["payments.create", "payments.read"],
    )
)
# key["data"]["api_key"] → save this, it is only shown once

# List API keys
keys = client.api_keys.list()

# Update an API key
client.api_keys.update(1, UpdateApiKeyParams(name="Renamed", status="inactive"))

# Delete an API key
client.api_keys.delete(1)
```

---

## Webhook Verification

PayNexus signs every webhook with HMAC-SHA256. Verify incoming webhooks to ensure they are authentic.

### Manual verification

```python
event = client.verify_webhook(
    raw_body=request.body.decode(),          # Raw body string
    signature=request.headers["X-PayNexus-Signature"],
    timestamp=request.headers["X-PayNexus-Timestamp"],
)

if event["event"] == "payment.completed":
    print("Payment completed:", event["data"])
```

### Standalone function

```python
from paynexus_gateway.webhooks import verify_webhook_signature

event = verify_webhook_signature(
    raw_body=raw_body,
    signature=signature,
    timestamp=timestamp,
    secret="whsec_...",
    tolerance=300,  # Max age in seconds (default: 300 = 5 min)
)
```

### Django example

```python
# views.py
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from paynexus_gateway import PayNexus

client = PayNexus(
    secret_key="sk_live_...",
    webhook_secret="whsec_...",
)

@csrf_exempt
def webhook(request):
    event = client.verify_webhook(
        raw_body=request.body.decode(),
        signature=request.headers.get("X-PayNexus-Signature", ""),
        timestamp=request.headers.get("X-PayNexus-Timestamp", ""),
    )

    if event["event"] == "payment.completed":
        # Handle payment completion
        pass

    return JsonResponse({"received": True})
```

### FastAPI example

```python
from fastapi import FastAPI, Request
from paynexus_gateway import PayNexus

app = FastAPI()
client = PayNexus(
    secret_key="sk_live_...",
    webhook_secret="whsec_...",
)

@app.post("/webhooks/paynexus")
async def webhook(request: Request):
    raw_body = (await request.body()).decode()
    event = client.verify_webhook(
        raw_body=raw_body,
        signature=request.headers.get("X-PayNexus-Signature", ""),
        timestamp=request.headers.get("X-PayNexus-Timestamp", ""),
    )
    return {"received": True}
```

### Flask example

```python
from flask import Flask, request, jsonify
from paynexus_gateway import PayNexus

app = Flask(__name__)
client = PayNexus(
    secret_key="sk_live_...",
    webhook_secret="whsec_...",
)

@app.route("/webhooks/paynexus", methods=["POST"])
def webhook():
    event = client.verify_webhook(
        raw_body=request.get_data(as_text=True),
        signature=request.headers.get("X-PayNexus-Signature", ""),
        timestamp=request.headers.get("X-PayNexus-Timestamp", ""),
    )
    return jsonify(received=True)
```

---

## Error Handling

The SDK raises typed errors you can catch individually:

```python
from paynexus_gateway import (
    PayNexus,
    AuthenticationError,
    ValidationError,
    NotFoundError,
    RateLimitError,
    PayNexusError,
)

try:
    client.payments.initiate(...)
except AuthenticationError:
    # 401 — Invalid or missing API key
    pass
except ValidationError as e:
    # 422 — Invalid input; e.errors has field-level details
    print(e.errors)  # {"phone": ["Invalid phone number"]}
except NotFoundError:
    # 404 — Resource not found
    pass
except RateLimitError:
    # 429 — Too many requests
    pass
except PayNexusError as e:
    # Other API error; check e.status and e.code
    print(e.status, e.code)
```

---

## Configuration

```python
client = PayNexus(
    secret_key="sk_live_...",              # Required
    webhook_secret="whsec_...",            # For webhook verification
    base_url="https://paynexus.co.ke/api",  # Default
    timeout=30.0,                          # Request timeout in seconds (default: 30)
)
```

The client supports context manager usage for automatic cleanup:

```python
with PayNexus(secret_key="sk_live_...") as client:
    payment = client.payments.initiate(...)
```

---

## Authentication

The SDK authenticates using your secret API key (`sk_...`) sent via the `X-API-Key` header. Secret keys have write access (payments, webhooks, API key management). Never expose your secret key in client-side code.

---

## Type Hints

The SDK is fully typed and ships with a `py.typed` marker. All parameter and response types are available:

```python
from paynexus_gateway import (
    InitiatePaymentParams,
    ListPaymentsParams,
    RegisterWebhookParams,
    UpdateWebhookParams,
    CreateApiKeyParams,
    UpdateApiKeyParams,
    ValidatePhoneParams,
    MpesaInitiatePaymentParams,
    MpesaPaymentStatusParams,
    CreateInvoiceParams,
    UpdateInvoiceParams,
    CheckoutSessionParams,
    Payment,
    PaymentData,
    Webhook,
    WebhookPayload,
    Merchant,
    Business,
    PaymentAccount,
    ApiKey,
    ApiResponse,
    Pagination,
)
```

---

## License

MIT
