# Connect: your app, acting for a business its owner connected

An API key is yours, over the businesses **you** opened. Connect is how your app acts for a business that already exists on BillOS and belongs to someone else: an accounting system pulling a client's ledger, a booking platform issuing its merchants' receipts, an ERP recording expenses. The owner connects the business from a consent screen; your app receives a per-business token and hears the business's events; the owner can disconnect at any time. It is OAuth 2.0, authorization code with PKCE, and it looks like Stripe Connect because that is the shape platforms already know.

### Register an app

```bash
curl -X POST "$BASE/oauth-apps" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{
  "name": "חשבשבת בענן",
  "redirectUris": ["https://app.example/billos/callback", "http://localhost:3000/callback"],
  "scopes": ["businesses:read", "documents:read", "documents:write", "expenses:read", "export"],
  "homepageUrl": "https://app.example", "logoUrl": "https://app.example/logo.png"
}'
# 201 → { app: { clientId: "bkapp_live_…", status: "pending", ... }, clientSecret: "bksec_live_…" }
```

- The app lives under your API client: your limits, your metering, your event log and your webhook endpoints serve it.
- `clientSecret` is shown **once**. `POST /oauth-apps/:id/rotate-secret` replaces it (the old one stops on the spot; issued tokens keep working).
- `scopes` is the most the app will ever ask for, in the fine grain (section 1). `ocr` cannot be requested.
- Redirect URIs are matched **exactly**, https only, no fragments; `http://localhost` and `http://127.0.0.1` are allowed for development and may vary their port.
- A **live** app starts `pending`: its name and logo are shown to business owners as something to trust, so we approve it by hand (usually within a working day; we may ask for the homepage). A **sandbox** app (registered with a `bk_test_` key) is usable immediately against sandbox businesses. Everything below is identical in both.

The same app is manageable from the console (מפתחים → אפליקציות Connect) and from the terminal: `billos apps create --name "…" --redirect https://app.example/billos/callback --scopes documents:read,documents:write` prints the secret once and a ready authorize URL to walk the flow by hand.

### The flow

**1. Send the owner to authorize.**

```
GET https://api.billos.co.il/oauth/authorize
    ?response_type=code
    &client_id=bkapp_live_…
    &redirect_uri=https://app.example/billos/callback
    &scope=businesses:read documents:read documents:write export
    &state=<random, bound to the user's session on your side>
    &code_challenge=<BASE64URL(SHA256(code_verifier))>
    &code_challenge_method=S256
```

`state` is required. PKCE is optional for a confidential client and recommended; when a challenge is sent, the exchange must carry the verifier. BillOS validates the request, then shows the consent screen on dash.billos.co.il: the owner signs in (Google, or their existing console session), picks **which** of their businesses to connect, reads the scopes in Hebrew, and approves or declines. A request that fails validation with a valid `redirect_uri` comes back to it with `error=invalid_scope` / `invalid_request` / `unauthorized_client` and your `state`; a request whose `redirect_uri` is not registered is refused on a BillOS page and never redirected.

**2. Receive the code.**

```
https://app.example/billos/callback?code=bkac_…&state=…
```

Check `state`. The code is valid for **10 minutes**, once.

**3. Exchange it.**

```bash
curl -X POST https://api.billos.co.il/oauth/token \
  -d grant_type=authorization_code -d code=bkac_… \
  -d redirect_uri=https://app.example/billos/callback \
  -d client_id=bkapp_live_… -d client_secret=bksec_live_… \
  -d code_verifier=…
```

```json
{ "access_token": "bkat_live_…", "token_type": "Bearer", "expires_in": 3600,
  "refresh_token": "bkrt_live_…", "scope": "businesses:read documents:read documents:write export",
  "business_id": "cm…", "billos_mode": "live" }
```

Form-encoded or JSON; client credentials in the body or as HTTP Basic. Errors follow RFC 6749 (`{ "error": "invalid_grant", "error_description": "…" }`, `401` for `invalid_client`). **Store `business_id`**: it is the business the owner picked, and every call below is nested under it.

**4. Call the API with the token.**

```bash
curl -H "Authorization: Bearer bkat_live_…" "$BASE/businesses/$BUSINESS_ID/documents"
```

The token is a credential like a key, narrowed: every route under the connected business works with the granted scopes and refuses the rest (`403 missing scope: expenses:write`), any other business is `404`, and the client-level surface (keys, usage, apps, opening businesses) is `403`: those are your key's. Webhooks and the event log are the exception, narrowed to the connected business (Events and webhooks, below). Rate limits and metering are your client's.

Nothing is exempt from the scopes, including the business itself: `GET /businesses` (which for a token returns exactly the one connected business, with `access.via: "connect"`) is the profile, so it needs `businesses:read`, and so does the customer list. An app that only issues documents does not need either, which is why `business_id` comes back with the token: knowing where you are is not a read.

**5. Refresh.** Access tokens live an hour; refresh tokens 180 days.

```bash
curl -X POST https://api.billos.co.il/oauth/token \
  -d grant_type=refresh_token -d refresh_token=bkrt_live_… \
  -d client_id=bkapp_live_… -d client_secret=bksec_live_…
```

A refresh returns a **new pair and retires the old one**. For 30 seconds after that rotation the retired refresh token still works (it mints another pair), so two runs of one automation refreshing at the same moment do not end the connection. Presented again after that, a retired refresh token is the standard theft signal: every token of that connection is revoked and the owner has to reconnect. Handle `invalid_grant` on refresh by sending the owner through step 1 again.

### Your key sees the connected business too

You do not have to route every call through per-business tokens. Once a business is connected, **your own API key** lists it (`GET /businesses`, `access.via: "connect"`, `access.app` naming the app) and may call every route under it with the **grant's** scopes, not the key's. Store the `business_id` per merchant and use one credential for your whole integration, the way Stripe's platforms do. The tokens remain the standard route and the one to use when a connection must be isolated per tenant in your own storage.

### Events and webhooks

Your webhook endpoints receive the connected business's events exactly as they receive your own businesses' (section 11), filtered by what the grant covers: `document.*`, `recurring.*` and `quote.*` need `documents:read`, `expense.*` needs `expenses:read`, `export.*` needs `export`, `business.status_changed` and `ita.*` need `businesses:read`. Two events are Connect's own and always reach your client: `connection.created` when an owner connects (or reconnects) a business, `connection.revoked` when they disconnect it or you deauthorize. Both appear in `GET /events`, and `POST /webhooks/:id/replay` covers them.

**The access token can subscribe too**, which is the shape an automation platform needs: each user's trigger is its own endpoint, created with that user's token rather than with your key. With a Connect token, `POST /webhooks` pins the endpoint to the connected business (naming another is refused), accepts only events the grant is sent anyway (asking for `expense.recorded` on a grant without `expenses:read` is a `400` that says so), and `GET /webhooks`, `PATCH`, `DELETE`, `/test`, `/deliveries`, `/replay`, `GET /events` and `GET /events/:id` all see that business's rows and nothing else. Two users connected to the same app never see each other's endpoints or events. The endpoint cap counts per business, so it bounds each user and not the whole platform. Your key still sees every endpoint your client holds (`GET /webhooks?businessId=` narrows it). An endpoint outlives a revoked connection but receives nothing while the connection is gone, and resumes if the owner connects your app again.

### Ending a connection

- **The owner**, from the console (הגדרות → גישה ושיתוף → אפליקציות מחוברות) or `DELETE /businesses/:bid/connections/:id`: every token for that business is refused from the next request, the business leaves your key's list, and you receive `connection.revoked`.
- **You**: `POST /oauth/deauthorize` with `business_id` and your client credentials ends your side; `POST /oauth/revoke` with a `token` (RFC 7009) kills one token pair without ending the connection.
- **Re-consent** (the owner authorizes the same app again) updates the grant to the scopes asked for **this** time, narrower or wider, and issues a fresh pair; it never adds silently.
- **Deleting the app** (`DELETE /oauth-apps/:id`) revokes every connection under it.

### What a connected app can never do

Connect a business it was not granted, invite people to it, connect other apps to it, change its approval or plan, mint keys, read metering. Those belong to the owner and to your key respectively, and no scope names them. Every action taken through a token is recorded in the business's audit trail under the app's name.

### Sandbox

Register the app with a `bk_test_` key and it is `approved` at once. To walk the flow, open the authorize URL while signed in to the console as the developer: the consent page offers the businesses your **sandbox client** opened (`POST /sandbox/seed` makes one), and everything that follows runs against the sandbox database with `X-BillOS-Mode: sandbox` on every response. A sandbox app can only connect sandbox businesses and a live app only live ones.