Metadata-Version: 2.4
Name: bursapay-sdk
Version: 0.1.1
Summary: Official Python SDK for the BursaPay Developer Gateway API
Project-URL: Homepage, https://bursapay.com
Project-URL: Documentation, https://bursapay.com/api-documentation
Project-URL: Repository, https://github.com/bursapay/bursapay
Project-URL: Bug Tracker, https://github.com/bursapay/bursapay-python/issues
Author-email: BursaPay <dev@bursapay.com>
License: MIT
License-File: LICENSE
Keywords: bursapay,fintech,gateway,nigeria,payments
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.8
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: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Requires-Dist: httpx>=0.24.0
Provides-Extra: dev
Requires-Dist: black; extra == 'dev'
Requires-Dist: pytest-httpx>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# bursapay-sdk · Python

Official Python SDK for the [BursaPay](https://bursapay.com) Developer Gateway API.

```
pip install bursapay-sdk
```

**Version:** 0.2.0 · Python 3.8–3.12 · Sync & async · [PyPI](https://pypi.org/project/bursapay-sdk/)

---

## Quick start

```python
from bursapay import BursaPay

# Production
bp = BursaPay("sk_live_xxxx")

# Sandbox / test
bp = BursaPay("sk_test_xxxx")

# Local dev server
bp = BursaPay("sk_test_xxxx", base_url="http://localhost:8000/api/v1")
```

---

## Payments

```python
# Initialize — returns an authorization URL to redirect your customer to
payment = bp.payments.initialize(
    amount=5000,          # NGN, not kobo
    email="customer@example.com",
    currency="NGN",       # NGN | USD | GBP | KES
    metadata={"order_id": "ORD-123"},
)
print(payment["authorization_url"])

# Verify after redirect back
result = bp.payments.verify("BP-XXXX")
print(result["status"])   # "success" | "failed" | "pending"

# Retrieve a single payment
payment = bp.payments.retrieve("BP-XXXX")

# List with optional filters
page = bp.payments.list(status="success", page_size=20)

# Charge a saved authorization code (recurring)
bp.payments.charge(
    authorization_code="AUTH_xxx",
    email="customer@example.com",
    amount=5000,
)

# Charge saved card without redirect (card-on-file / one-click)
bp.payments.charge_saved_card(
    customer_reference="cust_abc123",
    authorization_code="AUTH_xxx",
    amount=5000,
)

# Schedule a future charge (must be >60 s in the future)
bp.payments.initialize(
    amount=5000,
    email="customer@example.com",
    charge_at="2025-12-31T09:00:00Z",
)

# Cancel a scheduled payment
bp.payments.cancel_schedule("BP-XXXX")

# Split payment
bp.payments.initialize(
    amount=10000,
    email="buyer@example.com",
    splits=[
        {"subaccount_code": "ACCT_abc123", "share": 0.8},
        {"subaccount_code": "ACCT_def456", "share": 0.2},
    ],
)

# Bulk initialize
bp.payments.bulk_initialize([
    {"amount": 1000, "email": "a@b.com", "reference": "BP-001"},
    {"amount": 2000, "email": "c@d.com", "reference": "BP-002"},
])
```

---

## Customers

```python
customer = bp.customers.create(email="jane@example.com", name="Jane Doe")
print(customer["customer_reference"])   # "cust_xxxx"

bp.customers.list()
bp.customers.list(q="jane")             # search by email, name, or phone
bp.customers.retrieve("cust_xxxx")
bp.customers.update("cust_xxxx", name="Jane Smith")
bp.customers.payments("cust_xxxx")      # payment history
bp.customers.delete("cust_xxxx")
```

---

## Transfers

```python
transfer = bp.transfers.initiate(
    amount=10000,
    bank_code="044",          # Access Bank
    account_number="0123456789",
    account_name="John Doe",
    narration="Vendor payout",
)
bp.transfers.retrieve(transfer["reference"])
bp.transfers.list(status="completed")

# Bulk payout
bp.transfers.bulk([
    {"amount": 5000, "bank_code": "058", "account_number": "0987654321", "account_name": "Vendor A"},
    {"amount": 8000, "bank_code": "011", "account_number": "1122334455", "account_name": "Vendor B"},
])
```

---

## Webhooks

```python
# Register an endpoint
wh = bp.webhooks.create(
    url="https://myapp.com/hooks/bursapay/",
    events=["payment.success", "payment.failed", "transfer.success"],
)
print(wh["secret"])   # store this to verify incoming payloads

bp.webhooks.list()
bp.webhooks.update(wh["id"], is_active=False)
bp.webhooks.delete(wh["id"])

# Inspect delivery logs
bp.webhooks.logs(wh["id"], status="failed")
bp.webhooks.log_detail(log_id=42)
bp.webhooks.retry(log_id=42)

# All valid event types
bp.webhooks.events()

# Send a test event
bp.webhooks.send_test(wh["id"], event="payment.success")

# Dead-letter queue — deliveries that exhausted all retries
dlq = bp.webhooks.dead_letters()
for entry in dlq["results"]:
    if not entry["replayed"]:
        bp.webhooks.replay_dead_letter(entry["id"])
```

### Verifying incoming webhook signatures

```python
# Django example
from bursapay import BursaPay
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
import json

@csrf_exempt
def bursapay_webhook(request):
    is_valid = BursaPay.verify_webhook_signature(
        request.body,
        request.headers.get("X-BursaPay-Signature", ""),
        "your_webhook_secret",
    )
    if not is_valid:
        return HttpResponse(status=401)

    event = json.loads(request.body)
    if event["event"] == "payment.success":
        ref = event["data"]["reference"]
        # fulfil order...
    return HttpResponse(status=200)
```

---

## Wallet

```python
bp.wallets.balance()
bp.wallets.ledger()                          # all entry types
bp.wallets.ledger(entry_type="credit")       # filter: credit | debit | fee | settlement | withdrawal | refund
bp.wallets.settlements()
bp.wallets.settlement("STL-xxxx")
```

---

## Virtual Accounts

```python
# Default bank
va = bp.virtual_accounts.create("cust_xxxx")

# Choose a specific bank: wema-bank | access-bank | titan-paystack | sterling-bank
va = bp.virtual_accounts.create("cust_xxxx", preferred_bank="wema-bank")
print(va["account_number"], va["bank_name"])

bp.virtual_accounts.list()
bp.virtual_accounts.retrieve(va["id"])
```

---

## Subscriptions

```python
plan = bp.subscriptions.create_plan(
    name="Pro Monthly",
    amount=5000,
    interval="monthly",
)

sub = bp.subscriptions.enroll(
    customer_reference="cust_xxxx",
    plan_id=plan["id"],
    authorization_code="AUTH_xxx",
)

bp.subscriptions.pause(sub["id"])
bp.subscriptions.resume(sub["id"])
bp.subscriptions.cancel(sub["id"])
```

---

## Invoices

```python
inv = bp.invoices.create(
    customer_reference="cust_xxxx",
    line_items=[
        {"description": "Web design", "quantity": 1, "unit_price": 150000},
        {"description": "Hosting (annual)", "quantity": 1, "unit_price": 30000},
    ],
    due_date="2025-12-31",
)

bp.invoices.update(inv["reference"], status="sent")   # triggers payment link creation
bp.invoices.list(status="overdue")
bp.invoices.delete(inv["reference"])
```

---

## Payment Links

```python
link = bp.payment_links.create(
    title="Pay for Invoice #42",
    amount=180000,
    expires_at="2025-12-31T23:59:59Z",
)
print(link["url"])

bp.payment_links.analytics(link["link_code"])
```

---

## Refunds

```python
refund = bp.refunds.create("BP-XXXX", amount=2500, reason="Customer request")
bp.refunds.retrieve(refund["refund_reference"])
```

---

## Disputes

```python
# List all disputes
disputes = bp.disputes.list()
disputes = bp.disputes.list(per_page=10, cursor="...")

# Get a single dispute
dispute = bp.disputes.retrieve("DIS-XXXX")

# Submit evidence / update status
bp.disputes.update_evidence(
    "DIS-XXXX",
    evidence={
        "delivery_proof": "https://cdn.example.com/proof.pdf",
        "notes": "Item delivered on 2025-01-15",
    },
    status="under_review",
)
# NOTE: "won" and "lost" statuses are set by Paystack webhook events only —
#       the API will reject attempts to set them directly.
```

---

## Reconciliation

```python
# Requires a live secret key (sk_live_*)
# Rate-limited to 10 requests per hour per developer
result = bp.reconciliation.run("2025-07-31")
print(result["matched"])        # count of matching records
print(result["discrepancies"])  # list of mismatches to investigate
```

---

## Async support

```python
import asyncio
from bursapay import BursaPay

async def main():
    async with BursaPay("sk_test_xxxx").async_client() as bp:
        payment = await bp.payments.initialize(amount=5000, email="a@b.com")
        print(payment["authorization_url"])

asyncio.run(main())
```

---

## Error handling

```python
from bursapay import BursaPay
from bursapay.exceptions import (
    AuthenticationError,
    NotFoundError,
    ValidationError,
    RateLimitError,
    ServerError,
    BursaPayError,
)

try:
    bp.payments.initialize(amount=5000, email="x@y.com", currency="XYZ")
except ValidationError as e:
    print(e.error_code)       # "invalid_currency"
    print(e.field_errors)     # field-level errors dict
    print(e.status_code)      # 400
except AuthenticationError:
    print("Check your API key")
except NotFoundError as e:
    print(f"Not found: {e.message}")
except RateLimitError:
    print("Rate limited — back off and retry")
except ServerError as e:
    print(f"BursaPay server error: {e.status_code}")
except BursaPayError as e:
    print(f"Unexpected error: {e}")
```

---

## Context manager

```python
with BursaPay("sk_test_xxxx") as bp:
    result = bp.payments.verify("BP-XXXX")
# connection pool is closed automatically
```

---

## Environment variables (recommended)

```python
import os
from bursapay import BursaPay

bp = BursaPay(
    os.environ["BURSAPAY_SECRET_KEY"],
    base_url=os.environ.get("BURSAPAY_BASE_URL"),   # omit for production default
)
```

---

---

## License

MIT

```python
from bursapay import BursaPay

# Production
bp = BursaPay("sk_live_xxxx")

# Sandbox / test
bp = BursaPay("sk_test_xxxx")

# Local dev server
bp = BursaPay("sk_test_xxxx", base_url="http://localhost:8000/api/v1")
```

---

## Payments

```python
# Initialize — returns an authorization URL to redirect your customer to
payment = bp.payments.initialize(
    amount=5000,          # NGN, not kobo
    email="customer@example.com",
    currency="NGN",       # NGN | USD | GBP | KES
    metadata={"order_id": "ORD-123"},
)
print(payment["authorization_url"])

# Verify after Paystack redirects back
result = bp.payments.verify("BP-XXXX")
print(result["status"])   # "success" | "failed" | "pending"

# Retrieve a payment
payment = bp.payments.retrieve("BP-XXXX")

# List with optional filters
page = bp.payments.list(status="success", page_size=20)

# Charge a saved authorization code (recurring)
bp.payments.charge(
    authorization_code="AUTH_xxx",
    email="customer@example.com",
    amount=5000,
)

# Schedule a future charge (must be >60 s in the future)
bp.payments.initialize(
    amount=5000,
    email="customer@example.com",
    charge_at="2025-12-31T09:00:00Z",
)

# Cancel a scheduled payment
bp.payments.cancel_schedule("BP-XXXX")

# Split payment
bp.payments.initialize(
    amount=10000,
    email="buyer@example.com",
    splits=[
        {"subaccount_code": "ACCT_abc123", "share": 0.8},
        {"subaccount_code": "ACCT_def456", "share": 0.2},
    ],
)

# Bulk initialize
bp.payments.bulk_initialize([
    {"amount": 1000, "email": "a@b.com", "reference": "BP-001"},
    {"amount": 2000, "email": "c@d.com", "reference": "BP-002"},
])
```

---

## Customers

```python
customer = bp.customers.create(email="jane@example.com", name="Jane Doe")
print(customer["customer_reference"])   # "CUS-xxxx"

bp.customers.list()
bp.customers.retrieve("CUS-xxxx")
bp.customers.update("CUS-xxxx", name="Jane Smith")
bp.customers.payments("CUS-xxxx")
bp.customers.delete("CUS-xxxx")
```

---

## Transfers

```python
transfer = bp.transfers.initiate(
    amount=10000,
    bank_code="044",          # Access Bank
    account_number="0123456789",
    account_name="John Doe",
    narration="Vendor payout",
)
bp.transfers.retrieve(transfer["reference"])
bp.transfers.list(status="completed")

# Bulk payout
bp.transfers.bulk([
    {"amount": 5000, "bank_code": "058", "account_number": "0987654321", "account_name": "Vendor A"},
    {"amount": 8000, "bank_code": "011", "account_number": "1122334455", "account_name": "Vendor B"},
])
```

---

## Webhooks

```python
# Register an endpoint
wh = bp.webhooks.create(
    url="https://myapp.com/hooks/bursapay/",
    events=["payment.success", "payment.failed", "transfer.success"],
)
print(wh["secret"])   # store this to verify incoming payloads

bp.webhooks.list()
bp.webhooks.update(wh["id"], is_active=False)
bp.webhooks.delete(wh["id"])

# Inspect delivery logs
bp.webhooks.logs(wh["id"], status="failed")
bp.webhooks.retry(log_id=42)

# All valid event types
bp.webhooks.events()
```

### Verifying incoming webhook signatures

```python
# Django example
from bursapay import BursaPay
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
import json

@csrf_exempt
def bursapay_webhook(request):
    is_valid = BursaPay.verify_webhook_signature(
        request.body,
        request.headers.get("X-BursaPay-Signature", ""),
        "your_webhook_secret",
    )
    if not is_valid:
        return HttpResponse(status=401)

    event = json.loads(request.body)
    if event["event"] == "payment.success":
        ref = event["data"]["reference"]
        # fulfil order...
    return HttpResponse(status=200)
```

---

## Wallet

```python
bp.wallets.balance()
bp.wallets.ledger()
bp.wallets.settlements()
bp.wallets.settlement("STL-xxxx")
```

---

## Virtual Accounts

```python
va = bp.virtual_accounts.create("CUS-xxxx")
print(va["account_number"], va["bank_name"])

bp.virtual_accounts.list()
bp.virtual_accounts.retrieve(va["id"])
```

---

## Subscriptions

```python
plan = bp.subscriptions.create_plan(
    name="Pro Monthly",
    amount=5000,
    interval="monthly",
)

sub = bp.subscriptions.enroll(
    customer_reference="CUS-xxxx",
    plan_id=plan["id"],
    authorization_code="AUTH_xxx",
)

bp.subscriptions.pause(sub["id"])
bp.subscriptions.resume(sub["id"])
bp.subscriptions.cancel(sub["id"])
```

---

## Invoices

```python
inv = bp.invoices.create(
    customer_reference="CUS-xxxx",
    line_items=[
        {"description": "Web design", "quantity": 1, "unit_price": 150000},
        {"description": "Hosting (annual)", "quantity": 1, "unit_price": 30000},
    ],
    due_date="2025-12-31",
)

bp.invoices.update(inv["reference"], status="sent")   # triggers payment link creation
bp.invoices.list(status="overdue")
bp.invoices.delete(inv["reference"])
```

---

## Payment Links

```python
link = bp.payment_links.create(
    title="Pay for Invoice #42",
    amount=180000,
    expires_at="2025-12-31T23:59:59Z",
)
print(link["url"])

bp.payment_links.analytics(link["link_code"])
```

---

## Refunds

```python
refund = bp.refunds.create("BP-XXXX", amount=2500, reason="Customer request")
bp.refunds.retrieve(refund["refund_reference"])
```

---

## Async support

```python
import asyncio
from bursapay import BursaPay

async def main():
    async with BursaPay("sk_test_xxxx").async_client() as bp:
        payment = await bp.payments.initialize(amount=5000, email="a@b.com")
        print(payment["authorization_url"])

asyncio.run(main())
```

---

## Error handling

```python
from bursapay import BursaPay
from bursapay.exceptions import (
    AuthenticationError,
    NotFoundError,
    ValidationError,
    RateLimitError,
    ServerError,
    BursaPayError,
)

try:
    bp.payments.initialize(amount=5000, email="x@y.com", currency="XYZ")
except ValidationError as e:
    print(e.error_code)       # "invalid_currency"
    print(e.field_errors)     # field-level errors dict
except AuthenticationError:
    print("Check your API key")
except RateLimitError:
    print("Slow down — rate limited")
except ServerError as e:
    print(f"BursaPay server error: {e.status_code}")
except BursaPayError as e:
    print(f"Unexpected error: {e}")
```

---

## Context manager

```python
with BursaPay("sk_test_xxxx") as bp:
    result = bp.payments.verify("BP-XXXX")
# connection pool is closed automatically
```

---

## Environment variables (recommended)

```python
import os
from bursapay import BursaPay

bp = BursaPay(
    os.environ["BURSAPAY_SECRET_KEY"],
    base_url=os.environ.get("BURSAPAY_BASE_URL"),   # omit for production default
)
```
---

## License

MIT
