# Authentication

> OAuth2 with PKCE to talk to the Ghosty Studio API on behalf of a person — scopes, tokens, errors and how to get a client_id.

URL: https://www.ghosty.studio/en/docs/api/authentication

The API is **per person**: your application acts on behalf of someone who granted it permission. The mechanism is OAuth2 (`authorization_code` with PKCE), the same one the Ghosty mobile app uses.

## Endpoints

| Route | What it does |
|---|---|
| `GET /oauth2/authorize` | Consent screen. Returns a `code` to the `redirect_uri`. |
| `POST /oauth2/token` | Exchanges a `code` or `refresh_token` for tokens. |
| `POST /oauth2/revoke` | Revokes a token (RFC 7009). Always `200`. |
| `GET /oauth2/proveedores` | Lists the active login providers (Google, Apple). Public. |

## Scopes

| Scope | Allows |
|---|---|
| `profile` | Knowing who they are (id and email). |
| `agents:read` | Listing agents, reading conversations, subscribing to events. |
| `agents:write` | Sending messages, cancelling, answering permissions, scheduling, sharing, deleting the account. |
| `files` | Uploading and reading the account's files, transcribing audio. |

Request the ones you need separated by spaces. The server **narrows** them to your client's ceiling without failing: if you ask for `files` and your client doesn't have it, the token comes out without it. Scopes are **frozen** in the token; a new grant arrives only at the next login.

## Flow

1. Generate `code_verifier` (43–128 characters) and `code_challenge = BASE64URL(SHA256(code_verifier))`. Only `S256` is accepted.
2. Open in the browser:

```
https://www.ghosty.studio/oauth2/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.com/callback
  &scope=profile%20agents:read%20agents:write%20files
  &state=RANDOM
  &code_challenge=…
  &code_challenge_method=S256
```

3. The person signs in (Google, Apple or email) and accepts. They return to your `redirect_uri` with `?code=…&state=…`. The code is valid for **60 seconds**.
4. Exchange it:

```bash
curl -X POST https://www.ghosty.studio/oauth2/token \
  -d grant_type=authorization_code \
  -d code=THE_CODE \
  -d redirect_uri=https://your-app.com/callback \
  -d client_id=YOUR_CLIENT_ID \
  -d code_verifier=THE_VERIFIER
```

```json
{
  "access_token": "…",
  "refresh_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "profile agents:read agents:write files"
}
```

5. Use the token:

:::tabs
```bash tab=curl
curl https://www.ghosty.studio/api/v2/me/agents \
  -H "Authorization: Bearer ACCESS_TOKEN"
```
```typescript tab=TypeScript
const res = await fetch("https://www.ghosty.studio/api/v2/me/agents", {
  headers: { Authorization: `Bearer ${accessToken}` },
});
const { agentes } = await res.json(); // `agentes` = agents
```
```python tab=Python
import requests
r = requests.get("https://www.ghosty.studio/api/v2/me/agents",
                 headers={"Authorization": f"Bearer {access_token}"})
agentes = r.json()["agentes"]  # `agentes` = agents
```
:::

## Refreshing

The access token lasts **1 hour**; the refresh token, **90 days**, and it **rotates** on every use: store the new one and discard the old. Reusing an already-rotated refresh token revokes the whole family (it is the signal of a theft).

```bash
curl -X POST https://www.ghosty.studio/oauth2/token \
  -d grant_type=refresh_token \
  -d refresh_token=THE_REFRESH \
  -d client_id=YOUR_CLIENT_ID
```

Rule of thumb: `401` → refresh and retry once; `403` → a scope is missing, don't retry.

## Confidential and public clients

- **Public** (mobile, SPA): `client_id` only; PKCE mandatory.
- **Confidential** (backend): also `client_secret`, via `client_secret_post` or `Authorization: Basic`.

`redirect_uri` is compared **exactly**: no wildcards or sub-paths.

## Errors

| Code | When |
|---|---|
| `400 invalid_grant` | Expired code, wrong verifier, rotated refresh token, different `redirect_uri`. It deliberately doesn't distinguish the cause. |
| `401 invalid_client` | Unknown `client_id` or wrong secret. |
| `400 unsupported_grant_type` | Only `authorization_code` and `refresh_token`. |

## Getting a `client_id`

Client registration is manual while the API is in beta: write to [hola@ghosty.studio](mailto:hola@ghosty.studio) with your app's name, the exact `redirect_uri`s and the scopes you need. House clients (Teams, the mobile app) skip the consent screen; a third party always sees it.
