> ## Documentation Index
> Fetch the complete documentation index at: https://docs.busha.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Quick Start

> Go from zero to a working authorized API call in minutes. Steps 1–2 are dashboard setup; steps 3–6 are code.

<Note>
  This walkthrough assumes you have already [registered your OAuth2 app](./get-access) and have your `client_id` and `client_secret`. Pick one environment — sandbox or production — and use it throughout.
</Note>

## Step 1 — Generate PKCE and CSRF values

Your backend generates a fresh PKCE verifier and CSRF state for every authorization request. Never reuse them.

```bash theme={null}
# Generate code_verifier
CODE_VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')

# Derive code_challenge = base64url(sha256(code_verifier))
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')

# Generate CSRF state (at least 16 random bytes)
STATE=$(openssl rand -hex 16)
```

Store `CODE_VERIFIER` and `STATE` in the user's server-side session — you'll need them in steps 3 and 4.

## Step 2 — Redirect the user to Busha

Build the authorization URL and redirect the user's browser to it. This is the only step that touches the browser.

```
https://login.busha.io/oauth2/auth
  ?response_type=code
  &client_id=busha-oauth2-10d3cdb5-dda3-451a-87fc-9d6ccb0bb691-live
  &redirect_uri=https%3A%2F%2Fyourapp.example.com%2Foauth2%2Fcallback
  &scope=openid+offline_access+balances%3Aread+transactions%3Aread
  &state=YOUR_STATE
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256
```

| Parameter               | Required | Value                                      |
| ----------------------- | -------- | ------------------------------------------ |
| `response_type`         | ✓        | `code`                                     |
| `client_id`             | ✓        | Your app's client ID                       |
| `redirect_uri`          | ✓        | Byte-identical to a registered URI         |
| `scope`                 | ✓        | Space-separated list of [scopes](./scopes) |
| `state`                 | ✓        | CSRF guard — bind to the user's session    |
| `code_challenge`        | ✓        | `base64url(sha256(code_verifier))`         |
| `code_challenge_method` | ✓        | `S256` — `plain` is not accepted           |

The user sees Busha's hosted login and consent screens. When they click **Authorize**, Busha redirects their browser back to your callback.

***

## Step 3 — Handle the callback

Busha redirects the user to:

```
https://yourapp.example.com/oauth2/callback?code=AbCdEf123XYZ&state=YOUR_STATE
```

Before doing anything with the `code`:

1. **Verify `state`** matches the value you stored in step 1. If it doesn't match, reject the request — this is a CSRF attack.
2. **Deduplicate** the `code` if your callback could fire twice (e.g. the user double-clicks). Codes are single-use; a second exchange attempt returns `invalid_grant`.

If the user clicks **Cancel** on the consent screen, Busha sends `?error=access_denied&state=...` instead. Render a "continue without connecting" path.

## Step 4 — Exchange the code for tokens

This call is server-to-server only. Never expose your `client_secret` or the token response to the browser.

```bash theme={null}
curl -X POST https://login.busha.io/oauth2/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=$AUTH_CODE" \
  --data-urlencode "redirect_uri=https://yourapp.example.com/oauth2/callback" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"
```

Successful response:

```json theme={null}
{
  "access_token":  "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "rt_5dG9HaP2mQ8kVnY4...",
  "id_token":      "eyJhbGciOiJSUzI1NiIs...",
  "token_type":    "bearer",
  "expires_in":    3600,
  "scope":         "openid offline_access balances:read transactions:read"
}
```

| Token           | Returned when                      |
| --------------- | ---------------------------------- |
| `access_token`  | Always                             |
| `refresh_token` | `offline_access` scope was granted |
| `id_token`      | `openid` scope was granted         |

Store `access_token` and `refresh_token` securely. See [Token handling](./tokens) for storage requirements and rotation.

## Step 5 — Call the Busha API

Attach the access token as a Bearer header:

```bash theme={null}
curl https://api.busha.io/v1/balances?currency=NGN \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```json theme={null}
{
  "data": [
    { "currency": "NGN", "amount": "12345.67", "available": "12345.67" }
  ]
}
```

You are now calling Busha on the user's behalf.

## Step 6 — Keep the session alive with refresh

Access tokens expire in \~1 hour. Before `exp`, swap the refresh token for a new pair:

```bash theme={null}
curl -X POST https://login.busha.io/oauth2/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN"
```

<Warning>
  Refresh tokens rotate on every use. Persist the new `(access_token, refresh_token)` pair **before** discarding the old one. If persistence fails mid-call, you lose the session and must prompt the user to re-authorize. If you replay a previously-used refresh token, the entire token family is revoked immediately.
</Warning>

## What's next

<CardGroup cols={2}>
  <Card title="Scopes reference" icon="list" href="./scopes">
    Choose the right scopes for your integration and understand what each one does.
  </Card>

  <Card title="Token handling" icon="shield" href="./tokens">
    Validation, storage, rotation, and revocation in depth.
  </Card>

  <Card title="Endpoints" icon="server" href="./endpoints">
    Full reference for all OAuth2 protocol endpoints.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="./errors">
    Every error code, what causes it, and how to fix it.
  </Card>
</CardGroup>
