Metadata-Version: 2.5
Name: southbill
Version: 0.1.1
Summary: Official Python SDK for the Southbill API — checkout sessions, customers, invoices, payments, refunds, subscriptions, events and webhook signature verification.
Project-URL: Homepage, https://southbill.com
Project-URL: Repository, https://github.com/southbill/southbill-python
License: MIT
Keywords: api,checkout,invoices,payments,southbill
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# southbill (Python)

Official Python SDK for the [Southbill](https://southbill.com) API. No third-party
dependencies — standard library only. Requires Python 3.8+.

```sh
pip install southbill
```

## Quick start

```python
from southbill import Southbill

southbill = Southbill()  # or Southbill("sk_live_...")

session = southbill.checkout.sessions.create(
    amount=4900,
    currency="EUR",
    customer_email="ada@acme.com",
    success_url="https://acme.com/thanks",
)

print(session["checkout_url"])
```

The Merchant API accepts **live keys only** (`sk_live_...` for server calls, `pk_live_...`
for publishable/browser use). Legacy `sk_test_...` keys are rejected with
`401 authentication_error` — sandbox testing happens on the Developer Platform, not
through merchant keys.

Errors carry a machine-readable `error.type` plus an optional `error.code`:

| `type` | When |
| --- | --- |
| `authentication_error` | Missing, malformed, revoked or expired key |
| `permission_error` | Key lacks the required scope, or merchant is suspended |
| `invalid_request` | Bad input; `code: "resource_missing"` for unknown IDs (404) |
| `idempotency_error` | `code: "idempotency_key_reused"` (same key, different body) or `code: "idempotency_in_flight"` (same key still processing — retry shortly) |
| `rate_limit_error` | `code: "rate_limit_exceeded"` — retry after `Retry-After` |
| `already_refunded`, `charge_disputed` | Refund not possible for that charge |
| `product_limit_reached`, `account_not_ready`, `invalid_state` | Plan or account state blocks the call |
| `stripe_error`, `api_error` | Upstream or internal failure (502 / 500) |

Note: the App API (OAuth apps) uses `not_found` as an error type, while the Merchant API
returns `invalid_request` with `code: "resource_missing"` instead.

## Resources

| Namespace | Methods |
| --- | --- |
| `checkout.sessions` | `create`, `retrieve`, `list`, `expire` |
| `customers` | `create`, `retrieve`, `update`, `list`, `delete` |
| `invoices` | `create`, `retrieve`, `update`, `list`, `send`, `void`, `mark_paid`, `list_installments`, `list_payments` |
| `products` | `create`, `retrieve`, `update`, `list`, `delete`, `list_prices`, `create_price`, `set_default_price` |
| `payments` | `retrieve`, `list` |
| `refunds` | `create`, `retrieve`, `list` |
| `subscriptions` | `create`, `retrieve`, `update`, `list`, `cancel` |
| `subscription_links` | `create`, `retrieve`, `update`, `list`, `archive` |
| `events` | `retrieve`, `list`, `replay` |
| `webhook_endpoints` | `create`, `retrieve`, `update`, `list`, `delete`, `rotate_secret`, `list_deliveries` |
| `balance` | `retrieve`, `balance.transactions.retrieve`, `balance.transactions.list` |

Payouts, bank details, KYC and API-key management stay merchant-controlled in the
dashboard and are intentionally not part of the API surface.

### Installment payments (invoices)

```python
installments = southbill.invoices.list_installments("inv_123")
payments = southbill.invoices.list_payments("inv_123")
```

### Webhook endpoints (API-managed)

```python
endpoint = southbill.webhook_endpoints.create(
    url="https://acme.com/webhooks/southbill",
    enabled_events=["invoice.paid", "payment.succeeded"],
)
southbill.webhook_endpoints.rotate_secret(endpoint["id"])  # old secret stays valid 24 h
deliveries = southbill.webhook_endpoints.list_deliveries(endpoint["id"])
```

### Balance & transactions

```python
balance = southbill.balance.retrieve()
for tx in southbill.balance.transactions.auto_paging_iter(type="charge"):
    print(tx["bt_id"], tx["net"], tx["currency"])
```

## Idempotency

Every `POST` sends an `Idempotency-Key` header (random UUID). Pass your own for
safe retries across processes:

```python
southbill.invoices.create(idempotency_key=f"inv-{order_id}", customer="cus_123")
```

## Pagination

```python
for invoice in southbill.invoices.auto_paging_iter(status="open"):
    print(invoice["id"])
```

## Errors

Network errors, `429` and `5xx` are retried twice with exponential backoff.
Everything else raises `SouthbillError`:

```python
from southbill import SouthbillError

try:
    southbill.refunds.create(payment="pi_123", amount=500)
except SouthbillError as error:
    print(error.status, error.type, error.param, error.request_id)
```

## Webhooks

Verify the raw request body — never a re-serialized object.

```python
from flask import Flask, request
from southbill import construct_event, SouthbillSignatureError

app = Flask(__name__)

@app.post("/webhooks/southbill")
def webhook():
    try:
        event = construct_event(
            payload=request.get_data(),
            signature=request.headers.get("southbill-signature", ""),
            secret=os.environ["SOUTHBILL_WEBHOOK_SECRET"],
        )
    except SouthbillSignatureError:
        return "", 400

    if event["type"] == "invoice.paid":
        ...  # handle it

    return "", 200
```

Signature scheme: `Southbill-Signature: t=<unix seconds>,v1=<hex>` where the hex
digest is `HMAC-SHA256(secret, "<timestamp>.<raw body>")`. Default tolerance 300s.

## License

MIT
