# Installs & OAuth

The authorization code flow with PKCE, step by step.

Merchants install your app through OAuth 2.0 — authorization code with PKCE. No client secret is ever needed in a browser.

## 1. Send the merchant to consent

```text
https://southbill.com/oauth/authorize
  ?client_id=sb_client_live_123
  &redirect_uri=https://yourapp.com/callback
  &response_type=code
  &scope=merchant:read%20payments:read
  &state=<random>
  &code_challenge=<base64url(sha256(verifier))>
  &code_challenge_method=S256
```

The merchant sees your icon, name and a plain-language list of every scope with its risk level, then approves or declines.

## 2. Exchange the code

```bash
curl -X POST https://api.southbill.com/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "ac_…",
    "redirect_uri": "https://yourapp.com/callback",
    "client_id": "sb_client_live_123",
    "code_verifier": "…"
  }'
```

```node
const res = await fetch("https://api.southbill.com/v1/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "authorization_code",
    code,
    redirect_uri: "https://yourapp.com/callback",
    client_id: "sb_client_live_123",
    code_verifier: verifier,
  }),
});
const tokens = await res.json(); // { access_token, refresh_token, installation_id, … }
```

```python
tokens = requests.post(
    "https://api.southbill.com/v1/oauth/token",
    json={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": "https://yourapp.com/callback",
        "client_id": "sb_client_live_123",
        "code_verifier": verifier,
    },
    timeout=30,
).json()
```

```php
<?php
$ch = curl_init("https://api.southbill.com/v1/oauth/token");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode([
        "grant_type" => "authorization_code",
        "code" => $code,
        "redirect_uri" => "https://yourapp.com/callback",
        "client_id" => "sb_client_live_123",
        "code_verifier" => $verifier,
    ]),
]);
$tokens = json_decode(curl_exec($ch), true);
```

```json
{
  "access_token": "sbat_live_…",
  "refresh_token": "sbrt_live_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "merchant:read payments:read",
  "installation_id": "inst_live_7c1f92a8",
  "environment": "live"
}
```

Store the tokens per installation, never globally.

## 3. Refresh

```json
{ "grant_type": "refresh_token", "refresh_token": "sbrt_live_…", "client_id": "sb_client_live_123" }
```

Refresh tokens rotate: the response contains a new refresh token and the old one dies. If a refresh returns `invalid_grant`, treat the installation as gone and stop calling.

## Redirect URI rules

- HTTPS only, exact match, no wildcards. `http://localhost` is allowed in test mode.
- Up to five URIs per app.
- Changing a URI on a published app requires a new version.

## Uninstall

When a merchant uninstalls, tokens are revoked immediately and you receive an `app.uninstalled` webhook. Delete the merchant's data within 30 days unless law requires otherwise.

## Suspension

If a merchant is suspended, API calls for that installation return `403 merchant_suspended`. Back off and retry later — do not delete data on a suspension.
