Ghosty
API

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.

Updated 2026-09-16

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

RouteWhat it does
GET /oauth2/authorizeConsent screen. Returns a code to the redirect_uri.
POST /oauth2/tokenExchanges a code or refresh_token for tokens.
POST /oauth2/revokeRevokes a token (RFC 7009). Always 200.
GET /oauth2/proveedoresLists the active login providers (Google, Apple). Public.

Scopes

ScopeAllows
profileKnowing who they are (id and email).
agents:readListing agents, reading conversations, subscribing to events.
agents:writeSending messages, cancelling, answering permissions, scheduling, sharing, deleting the account.
filesUploading 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
  1. 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.
  2. 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"}
  1. Use the token:
bash
curl https://www.ghosty.studio/api/v2/me/agents \  -H "Authorization: Bearer ACCESS_TOKEN"
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
import requestsr = 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

CodeWhen
400 invalid_grantExpired code, wrong verifier, rotated refresh token, different redirect_uri. It deliberately doesn't distinguish the cause.
401 invalid_clientUnknown client_id or wrong secret.
400 unsupported_grant_typeOnly authorization_code and refresh_token.

Getting a client_id

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