Metadata-Version: 2.4
Name: tangentopay
Version: 0.8.0
Summary: Official Python SDK for the TangentoPay API
Project-URL: Homepage, https://tangentopay.com
Project-URL: Documentation, https://docs.tangentopay.com
Project-URL: Repository, https://github.com/Grut-Design-Agency/tangentopay-python
Project-URL: Bug Tracker, https://github.com/Grut-Design-Agency/tangentopay-python/issues
Author-email: TangentoPay <dev@tangentopay.com>
License: MIT License
        
        Copyright (c) 2026 TangentoPay
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: africa,fintech,payments,stripe,tangentopay
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: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: httpx<1.0.0,>=0.27.0
Provides-Extra: async
Requires-Dist: httpx[http2]>=0.27.0; extra == 'async'
Provides-Extra: dev
Requires-Dist: mypy>=1.10.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Description-Content-Type: text/markdown

# tangentopay-python

Official Python SDK for the [TangentoPay](https://tangentopay.com) API — accept payments, issue refunds, manage wallets, and verify webhooks with a clean, type-safe interface.

[![PyPI version](https://badge.fury.io/py/tangentopay.svg)](https://pypi.org/project/tangentopay/)
[![CI](https://github.com/Grut-Design-Agency/tangentopay-python/actions/workflows/ci.yml/badge.svg)](https://github.com/Grut-Design-Agency/tangentopay-python/actions/workflows/ci.yml)
[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

---

## Table of contents

- [Requirements](#requirements)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Authentication](#authentication)
- [Token expiry and refresh](#token-expiry-and-refresh)
- [Resources](#resources)
- [Provider status](#provider-status)
- [Currency and provider guide](#currency-and-provider-guide)
- [Service wallet operations (B2B2C)](#service-wallet-operations-b2b2c)
- [Payouts](#payouts)
- [Merchant wallet top-up](#merchant-wallet-top-up)
- [Payment methods](#payment-methods)
- [Async support](#async-support)
- [Error handling](#error-handling)
- [Webhook verification](#webhook-verification)
- [Security](#security)
- [License](#license)

---

## Requirements

- Python 3.9 or later
- A TangentoPay account — [sign up](https://tangentopay.com)

---

## Installation

```bash
pip install tangentopay

# with async support (HTTP/2 via httpx)
pip install "tangentopay[async]"
```

---

## Quick start

```python
import tangentopay

# ── Storefront: create a Stripe checkout session ──────────────────────────────
service = tangentopay.ServiceClient(service_key="pk_live_<your_service_key>")

session = service.checkout.create(
    products=[{"name": "Pro Plan", "price": 49.99, "quantity": 1}],
    currency_code="USD",
    customer_email="buyer@example.com",
    return_url="https://myshop.com/thank-you",
)
# redirect your customer to session.redirect_url

# ── Backend: manage payments with an API token ────────────────────────────────
merchant = tangentopay.MerchantClient(api_token="<your_bearer_token>")

payments = merchant.payments.list(per_page=20)
balance  = merchant.wallets.main_balance()
```

---

## Authentication

TangentoPay has two credential types:

| Credential | Where it goes | Client to use |
|---|---|---|
| **Service Key** (`pk_live_…` / `pk_test_…`) | `X-Service-Key` header | `ServiceClient` |
| **API Token** (Bearer) | `Authorization: Bearer …` | `MerchantClient` |

**Never expose an API token in browser or mobile code.** Use `ServiceClient` on the frontend and `MerchantClient` only on your server.

### ServiceClient

```python
service = tangentopay.ServiceClient(
    service_key="pk_live_<your_service_key>",
    # optional:
    base_url="https://api.tangentopay.com/api/v1",
    timeout=30.0,
    max_retries=3,
)
```

### MerchantClient

```python
merchant = tangentopay.MerchantClient(api_token="<your_bearer_token>")
```

Obtain an API token programmatically:

```python
client = tangentopay.MerchantClient()
client.auth.login(email=email, password=password)
token = client.auth.verify_otp(email=email, otp=otp)
merchant = tangentopay.MerchantClient(api_token=token.access_token)
```

---

## Token expiry and refresh

Catch `AuthenticationError` and re-authenticate in place:

```python
from tangentopay import AuthenticationError

try:
    payments = merchant.payments.list()
except AuthenticationError:
    merchant.auth.login(email=email, password=password)
    token = merchant.auth.verify_otp(email=email, otp=otp)
    merchant.set_token(token.access_token)  # updates all resources in place
    payments = merchant.payments.list()     # retry
```

---

## Resources

### ServiceClient resources

| Attribute | Description |
|---|---|
| `service.checkout` | Create Stripe-hosted checkout sessions; poll payment status |
| `service.topups` | Collect money from a customer's MoMo account into the service wallet |
| `service.withdrawals` | Send money from the service wallet to a customer's MoMo account |
| `service.provider_status` | Real-time health for MTN MoMo, Orange Money, and Stripe |

### MerchantClient resources

| Attribute | Description |
|---|---|
| `merchant.auth` | Login, OTP verification, profile, logout |
| `merchant.payments` | View and search your incoming payment history |
| `merchant.refunds` | Issue refunds on completed payments |
| `merchant.topups` | Top up your main wallet via card or MoMo |
| `merchant.payouts` | Send funds out (bank, MoMo, TP wallet, debit card) |
| `merchant.wallets` | Main and service wallet balances |
| `merchant.services` | View services; manage enabled payment methods per service |
| `merchant.customers` | Create and manage customer records |
| `merchant.analytics` | Payment summaries, revenue, and volume over time |
| `merchant.logs` | Per-service API request logs |
| `merchant.transfers` | Internal wallet transfer history |
| `merchant.provider_status` | Real-time health for MTN MoMo, Orange Money, and Stripe |

> **Note on service administration:** Creating services, rotating API keys, updating webhooks, and other one-time setup tasks are done from the [TangentoPay Dashboard](https://tangentopay.com). These operations are intentionally not exposed in the SDK.

---

## Provider status

Check provider health **before** initiating any collection or disbursement — this lets you show users a clear error message instead of a silent payment failure.

```python
status = merchant.provider_status.get()
# or: service.provider_status.get()

# status is a dict keyed by provider slug:
# {
#   "mtn_momo":     ProviderStatusEntry(slug="mtn_momo",     name="MTN Mobile Money", status="operational", ...),
#   "orange_money": ProviderStatusEntry(slug="orange_money", name="Orange Money",      status="degraded",    ...),
#   "stripe":       ProviderStatusEntry(slug="stripe",       name="Stripe",            status="operational", ...),
# }

if status["mtn_momo"].status == "down":
    raise PaymentUnavailableError(
        "MTN Mobile Money is currently unavailable. Try Orange Money or pay by card."
    )
```

Possible `status` values:

| Value | Meaning |
|---|---|
| `"operational"` | Fully functional — proceed normally |
| `"degraded"` | Partial outage — expect higher failure rates |
| `"down"` | Provider unreachable — do not attempt payments |

---

## Currency and provider guide

| Provider | Supported currencies | Notes |
|---|---|---|
| **MTN Mobile Money** | XAF only | Cameroon; USSD push via Fapshi. Min 100 XAF, max 500 000 XAF. |
| **Orange Money** | XAF only | Cameroon; USSD push via Fapshi. Min 100 XAF, max 500 000 XAF. |
| **Stripe** | USD, EUR, GBP, and more | Multi-currency card checkout and instant payouts. |

When a customer pays via MoMo the transaction currency is **XAF**. When they pay via Stripe card the currency is whatever `currency_code` you pass to `checkout.create()`.

Use `merchant.wallets.main_balance()` to get per-currency balances — the response includes a `balances` list showing only currencies with a non-zero funded amount, which you can use to build a currency-selector UI in your withdrawal flow.

---

## Service wallet operations (B2B2C)

The service wallet is funded when customers pay through your service's checkout flow.

```python
# ── Collect from a customer's MoMo ───────────────────────────────────────────
topup = service.topups.create(
    amount=5000,               # XAF
    customer_phone="237XXXXXXXXX",
    external_ref="ORDER-001",
    notify_url="https://yourapp.com/webhooks/momo",
)
# pending — wallet credited after Fapshi webhook confirms

# ── Disburse to a customer's MoMo ────────────────────────────────────────────
withdrawal = service.withdrawals.create(
    amount=4000,               # XAF
    recipient_phone="237XXXXXXXXX",
    external_ref="PAYOUT-001",
)
```

---

## Payouts

Two-step flow: initiate → confirm.

```python
# Step 1 — initiate
initiation = merchant.payouts.initiate(
    amount=50_000,
    currency_code="XAF",
    recipient_type="tangentopay_wallet",
    recipient_details={"wallet_address": "user@example.com"},
    note="Freelance payment",
)

# Step 2 — confirm with payout PIN
merchant.payouts.confirm(initiation.payout_ref, pin=os.environ["PAYOUT_PIN"])
```

### Virtual card payout (USD, Instant Payout)

```python
# Option A: use a saved card
merchant.payouts.initiate(
    amount=100,
    currency_code="USD",
    recipient_type="virtual_card",
    recipient_details={"payout_method_id": "pm_..."},
)

# Option B: one-time Stripe.js token (card never stored)
merchant.payouts.initiate(
    amount=100,
    currency_code="USD",
    recipient_type="virtual_card",
    recipient_details={"stripe_token_id": "tok_..."},
)
```

### Bulk payout

```python
with open("payouts.csv", "rb") as f:
    batch = merchant.payouts.bulk.initiate(
        csv_file=f,
        default_recipient_type="tangentopay_wallet",
    )
merchant.payouts.bulk.confirm(batch.batch_ref, pin=os.environ["PAYOUT_PIN"])
```

---

## Merchant wallet top-up

```python
# Via MoMo
topup = merchant.topups.create(
    amount=100_000,        # XAF
    phone="237XXXXXXXXX",
    provider="mtn_momo",
)

# Via card (Stripe-hosted page)
card_topup = merchant.topups.create_card_topup(
    amount=200,
    currency_code="USD",
    return_url="https://dashboard.yourapp.com/wallet",
)
```

---

## Payment methods

```python
methods = merchant.services.list_payment_methods(service_id)
# [{"slug": "mtn_momo", "name": "MTN Mobile Money", "enabled": True, "locked": False, ...}, ...]

# Disable Orange Money if provider is down
status = merchant.provider_status.get()
if status["orange_money"].status == "down":
    merchant.services.set_payment_method(service_id, "orange_money", enabled=False)

# Replace entire set (card must always be included)
merchant.services.set_payment_methods(service_id, ["card", "mtn_momo"])
```

---

## Async support

Every resource has an async equivalent — use `AsyncServiceClient` and `AsyncMerchantClient`:

```python
import asyncio
import tangentopay

async def main():
    service  = tangentopay.AsyncServiceClient(service_key="pk_live_<your_service_key>")
    merchant = tangentopay.AsyncMerchantClient(api_token="<your_bearer_token>")

    status   = await merchant.provider_status.get()
    payments = await merchant.payments.list()
    session  = await service.checkout.create(
        products=[{"name": "Pro Plan", "price": 49.99, "quantity": 1}],
        currency_code="USD",
        customer_email="buyer@example.com",
        return_url="https://myshop.com/thank-you",
    )

asyncio.run(main())
```

---

## Error handling

```python
from tangentopay import (
    AuthenticationError,   # 401
    PermissionError,       # 403
    NotFoundError,         # 404
    ValidationError,       # 422 — includes .errors dict
    RateLimitError,        # 429 — includes .retry_after seconds
    ServerError,           # 5xx
    NetworkError,          # connection-level failure
    TangentoPayError,      # base class for all above
)

try:
    merchant.payouts.initiate(...)
except ValidationError as e:
    print(e.errors)          # field-level validation messages
except RateLimitError as e:
    print(f"Retry after {e.retry_after}s")
except TangentoPayError as e:
    raise
```

---

## Webhook verification

```python
from tangentopay import Webhook, WebhookSignatureError

@app.route("/webhooks/tangentopay", methods=["POST"])
def handle_webhook():
    try:
        event = Webhook.construct_event(
            payload=request.data,
            signature=request.headers["X-TangentoPay-Signature"],
            secret=os.environ["TANGENTOPAY_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError:
        return "Bad signature", 400

    if event.event == "transaction.payment_completed":
        fulfill_order(event.payload)

    return {"received": True}
```

---

## Security

Please report security vulnerabilities to **security@tangentopay.com** rather than opening a public issue.

---

## License

[MIT](LICENSE)
