Metadata-Version: 2.4
Name: tangentopay
Version: 0.1.2
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)
- [Resources](#resources)
- [Async support](#async-support)
- [Error handling](#error-handling)
- [Webhook verification](#webhook-verification)
- [Supported currencies](#supported-currencies)
- [Security](#security)
- [License](#license)

---

## Requirements

- Python 3.9 or higher
- [`httpx`](https://www.python-httpx.org/) — installed automatically as a dependency

---

## Installation

```bash
pip install tangentopay
```

---

## Quick start

### 1. Accept a customer payment (storefront)

Use `ServiceClient` with your **public service key** (`pk_live_...`).
Get it from: **TangentoPay Dashboard → Services → your service → API Keys**.

```python
import tangentopay

client = tangentopay.ServiceClient("pk_live_your_service_key")

session = client.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",
    cancel_url="https://myshop.com/cart",
)

# Redirect your customer to the hosted checkout page
redirect(session.redirect_url)
```

### 2. Confirm payment before fulfilling an order

```python
# On your /thank-you page the URL contains ?session_id=...
# Poll until the payment is confirmed (up to 60 seconds)

status = client.checkout.wait_for_completion(transaction_uid, timeout=60)
if status.is_completed:
    fulfill_order()
```

### 3. Manage payments on the backend (merchant)

Use `MerchantClient` with your **API token** — keep this server-side only, never expose it in a browser.

```python
import os
import tangentopay

merchant = tangentopay.MerchantClient(api_token=os.environ["TANGENTOPAY_API_TOKEN"])

# List recent payments
page = merchant.payments.list(per_page=20)
for txn in page.data:
    print(txn.transaction_uid, txn.transaction_status, txn.final_amount)

# Issue a refund
refund = merchant.refunds.create(
    transaction_uid="TXN-ABC123",
    amount=49.99,
    reason="Customer request",
    pin="1234",
    recipient_type="stripe",
)

# Check wallet balance
balance = merchant.wallets.main_balance()
print(balance.available_balance, balance.currency_code)
```

### 4. Verify incoming webhooks

Always verify the HMAC signature before trusting any webhook payload.

```python
import os
import tangentopay

WEBHOOK_SECRET = os.environ["TANGENTOPAY_WEBHOOK_SECRET"]

def handle_webhook(raw_body: bytes, signature_header: str):
    try:
        event = tangentopay.Webhook.construct_event(
            payload=raw_body,
            signature=signature_header,
            secret=WEBHOOK_SECRET,
        )
    except tangentopay.WebhookSignatureError:
        return 400  # reject tampered or replayed events

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

    return 200
```

---

## Authentication

TangentoPay uses two separate credentials depending on what you are doing:

| Client | Credential | Header sent | When to use |
|---|---|---|---|
| `ServiceClient` | Service key (`pk_live_...`) | `X-Service-Key` | Creating checkout sessions, checking payment status — storefront / frontend server |
| `MerchantClient` | API token (Bearer) | `Authorization: Bearer` | Everything sensitive — payments, refunds, payouts, wallets, analytics — backend only |

### Getting your credentials

1. Log in to the [TangentoPay Dashboard](https://tangentopay.com)
2. Go to **Services** and open your service
3. Click **API Keys**
4. Copy the **Service Key** and **API Token**
5. Store them as environment variables — never commit them to git

```bash
# .env (never commit this file)
TANGENTOPAY_SERVICE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxxxxx
TANGENTOPAY_API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6...
TANGENTOPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
```

### Obtaining a token programmatically

```python
# Step 1 — submit credentials (triggers OTP to your registered device)
token = tangentopay.login(
    email="me@example.com",
    password="secret",
    otp="123456",
)

merchant = tangentopay.MerchantClient(api_token=token)
```

The token does not expire automatically. Call `merchant.auth.logout()` to revoke it.

---

## Resources

### `ServiceClient` resources

| Resource | Methods | Description |
|---|---|---|
| `checkout` | `create()`, `get_status()`, `wait_for_completion()` | Hosted Stripe checkout sessions |

### `MerchantClient` resources

| Resource | Methods | Description |
|---|---|---|
| `auth` | `login()`, `verify_otp()`, `me()`, `logout()`, `change_password()` | Authentication and profile |
| `payments` | `list()`, `get()`, `create_manual()` | View and record payments |
| `refunds` | `create()`, `list()` | Issue and list refunds |
| `topups` | `create()`, `list()` | Add funds to a wallet |
| `payouts` | `create()`, `bulk()`, `list()` | Send funds to recipients |
| `transfers` | `to_main()`, `list()` | Move funds between wallets |
| `wallets` | `main_balance()`, `service_balance()`, `manual_balance()` | Check balances |
| `services` | `list()`, `get()`, `create()`, `update()`, `delete()`, `create_api_key()`, `list_api_keys()`, `revoke_api_key()`, `update_webhook()` | Manage services and their keys |
| `customers` | `list()`, `get()`, `create()`, `update()`, `delete()`, `import_csv()` | Customer management |
| `analytics` | `dashboard()`, `payments_chart()`, `gross_volume()`, `total_payouts()` | Reporting and analytics |

---

## Async support

Every client has an async counterpart — `AsyncServiceClient` and `AsyncMerchantClient` — with identical methods that return awaitables. Use these with FastAPI, Starlette, or any `asyncio`-based framework.

```python
import asyncio
import tangentopay

async def main():
    client = tangentopay.AsyncServiceClient("pk_live_your_service_key")

    session = await client.checkout.create(
        products=[{"name": "Pro Plan", "price": 49.99, "quantity": 1}],
        currency_code="USD",
        return_url="https://myshop.com/thank-you",
        cancel_url="https://myshop.com/cart",
    )
    print(session.redirect_url)

asyncio.run(main())
```

```python
# FastAPI example
from fastapi import FastAPI, Request
import tangentopay

app = FastAPI()
merchant = tangentopay.AsyncMerchantClient(api_token=os.environ["TANGENTOPAY_API_TOKEN"])

@app.get("/payments")
async def list_payments():
    page = await merchant.payments.list(per_page=20)
    return {"total": page.total, "data": [p.transaction_uid for p in page.data]}
```

---

## Error handling

All SDK errors inherit from `tangentopay.TangentoPayError` so you can catch everything with one clause or be specific.

```python
try:
    refund = merchant.refunds.create(
        transaction_uid="TXN-001",
        amount=9999.00,
        reason="test",
        pin="wrong",
        recipient_type="stripe",
    )
except tangentopay.ValidationError as e:
    # Server-side field validation failed
    print(e.errors)           # {"amount": ["exceeds original transaction amount"]}
except tangentopay.AuthenticationError:
    # Token is invalid or expired — re-authenticate
    print("Invalid or expired token")
except tangentopay.PermissionError:
    # Authenticated but not allowed to perform this action
    print("Insufficient permissions")
except tangentopay.NotFoundError:
    print("Transaction not found")
except tangentopay.RateLimitError as e:
    # SDK already retried with exponential backoff and gave up
    print(f"Rate limited — retry after {e.retry_after}s")
except tangentopay.ServerError:
    # 5xx — SDK retried 3 times automatically before raising
    print("TangentoPay server error")
except tangentopay.NetworkError:
    # Timeout, DNS failure, connection refused
    print("Network error — check your connection")
except tangentopay.TangentoPayError as e:
    # Catch-all for any other SDK error
    print(f"Error {e.http_status}: {e.message}")
```

### Exception reference

| Exception | HTTP status | Notes |
|---|---|---|
| `AuthenticationError` | 401 | Invalid or expired API key / token |
| `PermissionError` | 403 | Authenticated but not authorised |
| `NotFoundError` | 404 | Resource does not exist |
| `ValidationError` | 422 | Field-level errors in `e.errors` dict |
| `RateLimitError` | 429 | After all retries exhausted; `e.retry_after` seconds |
| `ServerError` | 5xx | After 3 automatic retries |
| `NetworkError` | — | Timeout, DNS, connection error |
| `WebhookSignatureError` | — | Invalid HMAC, tampered payload, or replay attack |

---

## Webhook verification

TangentoPay signs every webhook with HMAC-SHA256 and includes a timestamp to prevent replay attacks. The SDK verifies both automatically.

```python
from tangentopay.webhook import Webhook
import tangentopay

event = Webhook.construct_event(
    payload=raw_body,           # bytes or str — the raw request body
    signature=sig_header,       # value of the X-TangentoPay-Signature header
    secret=webhook_secret,      # whsec_... from your dashboard
    timestamp_tolerance_seconds=300,  # default — reject events older than 5 minutes
)
```

**Signature header format:**

```
X-TangentoPay-Signature: t=1716134400,sha256=abcdef1234...
```

**Django example:**

```python
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from tangentopay.webhook import Webhook
import tangentopay

@csrf_exempt
def webhook(request):
    try:
        event = Webhook.construct_event(
            payload=request.body,
            signature=request.headers.get("X-TangentoPay-Signature", ""),
            secret=settings.TANGENTOPAY_WEBHOOK_SECRET,
        )
    except tangentopay.WebhookSignatureError as e:
        return HttpResponse(str(e), status=400)

    match event.event:
        case "transaction.payment_completed":
            handle_payment(event.payload["transaction_uid"])
        case "transaction.refund_completed":
            handle_refund(event.payload["transaction_uid"])

    return HttpResponse(status=200)
```

**Flask example:**

```python
from flask import Flask, request, abort
from tangentopay.webhook import Webhook
import tangentopay

app = Flask(__name__)

@app.post("/webhooks/tangentopay")
def webhook():
    try:
        event = Webhook.construct_event(
            payload=request.data,
            signature=request.headers.get("X-TangentoPay-Signature", ""),
            secret=app.config["TANGENTOPAY_WEBHOOK_SECRET"],
        )
    except tangentopay.WebhookSignatureError:
        abort(400)

    if event.event == "transaction.payment_completed":
        handle_payment(event.payload["transaction_uid"])

    return "", 200
```

### Supported webhook events

| Event | When it fires |
|---|---|
| `transaction.payment_completed` | Payment successfully processed |
| `transaction.payment_failed` | Payment attempt failed |
| `transaction.refund_completed` | Refund issued successfully |
| `transaction.payout_completed` | Payout sent to recipient |
| `transaction.topup_completed` | Wallet top-up completed |

---

## Supported currencies

TangentoPay supports Stripe's full currency list. Commonly used currencies:

| Code | Currency |
|---|---|
| `USD` | US Dollar |
| `EUR` | Euro |
| `GBP` | British Pound |
| `XAF` | Central African CFA Franc (Cameroon, Chad, Congo, Gabon…) |
| `NGN` | Nigerian Naira |
| `GHS` | Ghanaian Cedi |
| `KES` | Kenyan Shilling |
| `ZAR` | South African Rand |

> **XAF note:** Amounts in XAF are zero-decimal — pass `500` not `5.00`. The SDK handles this automatically when you provide `currency_code="XAF"`.

---

## Contributing

See [CONTRIBUTING.md](https://github.com/Grut-Design-Agency/tangentopay-python/blob/main/CONTRIBUTING.md) for development setup, branch naming, commit conventions, code style, and release instructions.

---

## Security

Security issues should **not** be reported via public GitHub issues.

Please report vulnerabilities by emailing **security@tangentopay.com**. We will acknowledge within 48 hours and aim to release a fix within 7 days for critical issues.

See [SECURITY.md](SECURITY.md) for the full security policy.

### Security features built into this SDK

- **HTTPS enforced** — the SDK rejects any `base_url` that does not use `https://`, preventing accidental credential leakage over plain HTTP
- **Header injection protection** — credentials are validated for CR/LF characters at construction time, preventing HTTP header injection attacks
- **Webhook replay protection** — `construct_event()` rejects events with timestamps outside a configurable tolerance window (default 5 minutes)
- **Payload size limit** — webhook payloads over 10 MB are rejected before any HMAC computation
- **Credential masking** — API keys and tokens are masked in `repr()` output so they do not appear in logs or debug output
- **Capped retry backoff** — the `Retry-After` value from the server is capped at 60 seconds to prevent server-controlled denial-of-service
- **Protected auth headers** — `extra_headers` cannot override `Authorization` or `X-Service-Key`

---

## License

MIT — see [LICENSE](LICENSE) for the full text.

---

<p align="center">
  Built with ❤️ by the <a href="https://tangentopay.com">TangentoPay</a> team
</p>
