# Endpoints

Everything the App API exposes today.

The App API is how your app reads and writes the merchant's data. Paths below are **relative to the base URL** — `/payments` means `https://api.southbill.com/v1/app/payments`. Payments, payouts, invoices, subscriptions and analytics are **read-only**. The Store surface (products, prices, options, values, variants, files, orders) also supports **writes** with the `products:write`, `files:write` and `orders:write` scopes — no merchant secret key is needed, the installation's `sbat_live_…` / `sbat_test_…` token is enough. Unsupported methods return `405 method_not_allowed`.

Write requests should send an `Idempotency-Key` header; a repeated key returns the original result instead of creating a second object.

Base URL: `https://api.southbill.com/v1/app` · Auth: `Authorization: Bearer sbat_live_…` (see [Authentication](/docs/dev/app-api/authentication)).

## Endpoints

| Method | Path | Scope | What it gives you |
| --- | --- | --- | --- |
| GET | `/merchant` | `merchant:read` | Profile of the merchant that installed your app: company, country, VAT number, status. Call once after install to label the connection. |
| GET | `/payments` | `payments:read` | Processed payments, newest first. Live traffic supports `?status=succeeded`. |
| GET | `/payments/{id}` | `payments:read` | One payment. The `{id}` is the `charge_id` in live and the row `id` in sandbox. |
| GET | `/refunds` | `refunds:read` | Sandbox: refund rows. Live: refunded charges with the running `amount_refunded`. |
| GET | `/disputes` | `disputes:read` | Chargebacks including `evidence_due_by` and `past_due`. |
| GET | `/payouts` | `payouts:read` | Bank settlements. Restricted scope — reconciliation tools only. |
| GET | `/balance` | `payouts:read` | Available and pending balance of the merchant. |
| GET | `/products` | `products:read` | The merchant's catalog (no prices in v1). |
| GET | `/products/{id}` | `products:read` | One product. |
| GET | `/invoices` | `invoices:read` | Issued invoices. **Live only** — sandbox returns an empty list. |
| GET | `/invoices/{id}` | `invoices:read` | One invoice. |
| GET | `/subscriptions` | `subscriptions:read` | Recurring agreements with status and period end. |
| GET | `/analytics/summary` | `analytics:read` | Aggregated volume for a window: `?days=30` (1–365). |

### Store write endpoints

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/store` | `store:read` | The merchant's Southbill Store: name, slug, branding, status. |
| POST | `/products` | `products:write` | Create a store product (name, description, images, active/visible flags). |
| PATCH | `/products/{id}` | `products:write` | Update a product, incl. `default_price_id`. |
| DELETE | `/products/{id}` | `products:write` | Archive a product (soft delete — stays referenced by past orders). |
| POST | `/products/{id}/prices` | `products:write` | Add a price (minor units + currency). |
| GET | `/products/{id}/options` | `products:read` | Option groups (Size, Colour) with values. |
| POST | `/products/{id}/options` | `products:write` | Create an option group. |
| PATCH | `/options/{id}` | `products:write` | Rename / reorder an option group. |
| DELETE | `/options/{id}` | `products:write` | Delete an option group — `409` while variants reference it. |
| POST | `/options/{id}/values` | `products:write` | Add an option value. |
| DELETE | `/values/{id}` | `products:write` | Delete a value — `409` while a variant uses it. |
| GET | `/products/{id}/variants` | `products:read` | Variants incl. their option combination. |
| POST | `/products/{id}/variants` | `products:write` | Create a variant. Duplicate combinations return `409`. |
| PATCH | `/variants/{id}` | `products:write` | Update price, stock, SKU or option combination. |
| DELETE | `/variants/{id}` | `products:write` | Delete a variant. The implicit default variant cannot be deleted. |
| POST | `/files` | `files:write` | Upload product images / documents (max 5 MB). |
| GET | `/orders` | `orders:read` | Store orders. Only Southbill Store payments create orders. |
| GET | `/orders/{id}` | `orders:read` | One order. |
| PATCH | `/orders/{id}` | `orders:write` | Update fulfillment status, tracking, carrier. |
| POST | `/orders/{id}/evidence` | `orders:write` | Attach delivery evidence. |
| POST | `/orders` | `orders:write` | **Sandbox only** — create an order fixture to test fulfillment. |

### Subscription links

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| GET | `/subscription_links` | `subscriptions:read` | List the merchant's subscription links. |
| GET | `/subscription_links/{id}` | `subscriptions:read` | One subscription link. |
| POST | `/subscription_links` | `subscriptions:write` | Create a subscription link (amount in minor units, currency, interval). |
| POST | `/subscription_links/{id}` | `subscriptions:write` | Update a subscription link. |
| POST | `/subscription_links/{id}/archive` | `subscriptions:write` | Archive a subscription link. |

Subscription links are only available on **live** installations; the merchant and store are always taken from the installation, never from the request body.

Ownership is always derived from the access token, never from ids in the body: an app can only touch the store it was installed on, in the environment of its token.

Field-by-field descriptions of every response are in [Objects & fields](/docs/dev/app-api/objects).

## Query parameters

| Parameter | Where | Behaviour |
| --- | --- | --- |
| `limit` | all list endpoints | 1–100, default 25. |
| `offset` | all list endpoints | Row offset, default 0. |
| `status` | `/payments` (live) | Exact match, e.g. `?status=succeeded`. |
| `days` | `/analytics/summary` | 1–365, default 30. |

Unknown query parameters are ignored, not rejected. There are no date-range filters in v1 — page by `offset` and stop at the first object older than your cursor.

## Pagination

```bash
curl "https://api.southbill.com/v1/app/payments?limit=50&offset=100" \
  -H "Authorization: Bearer sbat_live_…"
```

```node
async function* allPayments(accessToken) {
  for (let offset = 0; ; offset += 50) {
    const res = await fetch(
      `https://api.southbill.com/v1/app/payments?limit=50&offset=${offset}`,
      { headers: { Authorization: `Bearer ${accessToken}` } },
    );
    const page = await res.json();
    yield* page.data;
    if (!page.has_more) return;
  }
}
```

```python
def all_payments(access_token):
    offset = 0
    while True:
        page = requests.get(
            "https://api.southbill.com/v1/app/payments",
            params={"limit": 50, "offset": offset},
            headers={"Authorization": f"Bearer {access_token}"},
            timeout=30,
        ).json()
        yield from page["data"]
        if not page["has_more"]:
            return
        offset += 50
```

```php
<?php
$offset = 0;
do {
    $ch = curl_init("https://api.southbill.com/v1/app/payments?limit=50&offset={$offset}");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["Authorization: Bearer {$accessToken}"],
    ]);
    $page = json_decode(curl_exec($ch), true);
    foreach ($page["data"] as $payment) {
        // handle $payment
    }
    $offset += 50;
} while ($page["has_more"]);
```

```json
{ "object": "list", "data": [ … ], "has_more": true }
```

Lists are sorted newest first and carry no total count. Iterate until `has_more` is `false`. New objects arrive while you page, so for exports read the first page frequently rather than deep-paging a moving list.

## Test vs live

The token decides the data source: a sandbox token reads your environment's simulated data, a live token reads the merchant's real data. Field names differ between the two — see the table at the top of [Objects & fields](/docs/dev/app-api/objects).

## Amounts and timestamps

Amounts are integer minor units with a lowercase ISO currency: `{ "amount": 2500, "currency": "eur" }` is €25.00. Never use floats. Timestamps are **unix seconds** (UTC integers) — including `arrival_date`, `issue_date` and `due_date`, which are midnight UTC of the respective day. See [Objects & fields](/docs/dev/app-api/objects).

## Errors

```json
{ "error": { "type": "insufficient_scope", "message": "Missing required scope: payments:read" } }
```

See [Error & status codes](/docs/dev/dev-reference/errors).
