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

# OAuth Flows

> Connecting social accounts via platform OAuth and authenticating MCP clients via OAuth 2.1.

SocialAPI uses OAuth in two distinct contexts:

1. **Connecting platform accounts** (Instagram, Facebook, etc.) to your SocialAPI account so you can manage them through the API.
2. **Authenticating MCP/API clients** (Claude, ChatGPT, Cursor) via the built-in OAuth 2.1 authorization server so they can call the API on your behalf.

## Connecting platform accounts

SocialAPI manages platform OAuth on your behalf. Your application kicks off the connection, sends the user to the platform's consent screen, and receives a redirect back to a URL of your choice with the result. You never see or store platform access tokens — SocialAPI persists them encrypted and proxies all subsequent calls.

This is a managed connection pattern, not an OAuth flow you implement against SocialAPI. For OAuth 2.1 (used by MCP and third-party API clients calling SocialAPI), see the section below.

### Step 1: Initiate the connection

Call `POST /v1/accounts/connect` with the platform slug, your post-connection `redirect_uri`, and an optional `state` value for correlation:

```bash theme={null}
curl -X POST https://api.social-api.ai/v1/accounts/connect \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "instagram",
    "redirect_uri": "https://app.example.com/oauth/callback",
    "state": "session_abc123"
  }'
```

Fields:

| Field          | Location  | Required    | Notes                                                                                                                                                                                                                                                     |
| -------------- | --------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `platform`     | top-level | yes         | Platform slug (`instagram`, `facebook`, `threads`, `tiktok`, `linkedin`, `twitter`, `youtube`, `google`).                                                                                                                                                 |
| `redirect_uri` | top-level | recommended | Your post-connection callback. If you have registered any redirect URIs, this must match one of them (see [Whitelisting redirect URIs](#whitelisting-redirect-uris)). If omitted, the user sees a built-in HTML success page instead of being redirected. |
| `state`        | top-level | optional    | Opaque correlation value, max 512 chars, echoed back verbatim in the final redirect. Use it to tie the result to a session on your side.                                                                                                                  |
| `brand_id`     | top-level | optional    | Attach the account to an existing brand. If omitted, a new brand is auto-created.                                                                                                                                                                         |
| `metadata`     | top-level | optional    | Platform-specific connection data only (for example, TikTok BYOK uses this for `apikey`). For standard managed OAuth this is empty or omitted.                                                                                                            |

Response (HTTP 202):

```json theme={null}
{
  "auth_url": "https://www.instagram.com/oauth/authorize?client_id=...&state=...",
  "state": "4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2",
  "message": "Redirect the user to auth_url"
}
```

The `state` in the response is SocialAPI's internal CSRF token used between our connect call and our platform callback. Your application does not need to read or store it; just send the user to `auth_url`.

### Step 2: User authorizes on the platform

Redirect your user to the `auth_url`. They log in to the platform and grant permissions. The platform redirects back to **SocialAPI's** callback (not yours). SocialAPI exchanges the code for an access token, persists it encrypted, and then 302-redirects the user to **your** `redirect_uri`.

### Step 3: Receive the result at your `redirect_uri`

**Success:**

```
https://app.example.com/oauth/callback
  ?status=success
  &platform=instagram
  &state=session_abc123
  &account_id=acc_01HZ9X3Q4R5M6N7P8V2K0W1J
```

One connection produces exactly one `account_id`. Facebook accounts that manage multiple Pages still resolve to a single account in the connection result; Page details are exposed separately under `/v1/accounts/{id}/pages` and are not part of this flow.

**Error** (the user denied consent, the platform returned an error, or the link expired):

```
https://app.example.com/oauth/callback
  ?status=error
  &error=access_denied
  &error_description=The+user+denied+the+request
  &platform=instagram
  &state=session_abc123
```

Always check `status` first. On `success`, persist the `account_id` against your user record and call `/v1/accounts/{id}` for full account details. On `error`, surface a retry path in your UI — the customer can restart from Step 1.

### Google: connecting a login with multiple Business Profiles

This only applies to Google logins that manage more than one Business Profile. A Google login with a single Profile connects like any other platform, in a single request, with no extra step.

Google accounts can manage several Business Profiles under one login (for example, a chain with multiple locations). When that happens, SocialAPI cannot know which Profile you want to connect, so it does not create an account yet. Instead it stores a pending connection and asks you to choose.

**Server-callback flow.** If the Google login you just authorized manages more than one Profile, your `redirect_uri` receives `status=selection_required` instead of `status=success`, along with a `connection_id`:

```
https://app.example.com/oauth/callback
  ?status=selection_required
  &connection_id=pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2
  &state=session_abc123
```

**Proxy flow.** If your server exchanges the authorization code itself via `POST /v1/oauth/exchange`, the same fork happens in the response body. A single Profile still returns the usual `ConnectAccountResponse` (HTTP 201). More than one Profile returns HTTP 200 instead:

```json theme={null}
{
  "status": "selection_required",
  "connection_id": "pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2",
  "platform": "google",
  "profiles": [
    {
      "platform_account_id": "accounts/123/locations/456",
      "display_name": "Joe's Pizza (Downtown)",
      "username": "",
      "profile_picture_url": "",
      "bio": "Wood-fired pizza since 1998"
    },
    {
      "platform_account_id": "accounts/123/locations/789",
      "display_name": "Joe's Pizza (Uptown)",
      "username": "",
      "profile_picture_url": "",
      "bio": ""
    }
  ]
}
```

**Resolving the selection.** In both cases no account exists yet. Finish the connection with the `connection_id`:

1. `GET /v1/accounts/pending/{connection_id}` lists the candidate Profiles. This is usually optional since the callback redirect or the exchange response already includes them, but it is useful to re-fetch the list or check whether the pending connection is still valid.

```bash theme={null}
curl https://api.social-api.ai/v1/accounts/pending/pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2 \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

2. `POST /v1/accounts/pending/{connection_id}/select` with the `platform_account_id` of exactly one Profile connects it:

```bash theme={null}
curl -X POST https://api.social-api.ai/v1/accounts/pending/pc_4a8f2c1e9b3d7f06a5c2e8b4d1f3a7e2/select \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"platform_account_id": "accounts/123/locations/456"}'
```

This returns the same `ConnectAccountResponse` (HTTP 201) as a normal single-Profile connect.

<Note>
  A pending connection is single-use and expires 30 minutes after it is created. The first successful selection consumes it: a second `select` call, or one made after expiry, returns `404`. Selecting a `platform_account_id` that is not in the returned candidate set returns `400`.
</Note>

### Fallback when `redirect_uri` is omitted

If your connect call did not include `redirect_uri`, SocialAPI renders a built-in HTML success or error page in place of the redirect. This is intended for ad-hoc connections (for example, the SocialAPI dashboard itself); production integrations should always supply `redirect_uri`.

### Whitelisting redirect URIs

Redirect URI enforcement is optional until you register your first URI. If your workspace has no redirect URIs registered, any valid `redirect_uri` (https, or http for localhost) is accepted when starting a connect flow. As soon as you register one or more URIs, every `redirect_uri` must match an entry in your list, and unmatched values are rejected with a `400` error. Deleting all registered URIs returns your workspace to open mode.

Register URIs from the SocialAPI dashboard under **Settings → OAuth redirect URIs**. The management API exists at `/v1/oauth/redirect-uris` but is dashboard-only (requires the dashboard JWT, not an API key).

We recommend registering your production redirect URIs to lock down where users can be sent after OAuth.

### Correlation `state` (customer-controlled)

The top-level `state` field on `POST /v1/accounts/connect` is **your** correlation token, not a security primitive enforced by SocialAPI. It is echoed back unchanged in the final redirect. Common uses:

* Tie the redirect to the originating session (set `state` to a server-side session ID).
* CSRF defense for your own application — generate a random value, store it in the user's session, and verify it matches when the redirect lands.

SocialAPI does not validate `state`. If you do not pass one, the final redirect will include `state=` with an empty value.

### Listing connected accounts

```bash theme={null}
curl https://api.social-api.ai/v1/accounts \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

### Reconnecting an account

If you call `POST /v1/accounts/connect` for an account that is already connected, the flow runs again and the stored token is upserted. This is safe to call when tokens expire and is the supported path for token refresh on platforms that lack long-lived tokens.

### Disconnecting an account

`DELETE /v1/accounts/{id}` removes the account and the tokens we hold for it. Where the platform offers a token-revocation endpoint, we also revoke the grant on the platform during disconnect: Facebook, Google, Instagram, Threads, X, TikTok, and YouTube all support this.

LinkedIn, Pinterest, and Zalo do not offer a revocation API, so disconnect removes only the token stored on our side. On those platforms the user still sees the app authorized in their account settings and must remove it manually. If you offboard users under GDPR or similar rules, account for this manual step on those three platforms.

This is separate from `POST /oauth/revoke` below, which revokes SocialAPI's own OAuth 2.1 tokens issued to MCP clients, not a platform grant.

***

## OAuth 2.1 for MCP and API clients

SocialAPI includes a built-in OAuth 2.1 authorization server that enables MCP clients (Claude, ChatGPT, Cursor) and other third-party applications to authenticate users and call the API on their behalf.

The implementation follows these RFCs:

* **RFC 8414** for authorization server metadata discovery
* **RFC 7591** for dynamic client registration
* **RFC 7636** for PKCE (required)
* **RFC 7009** for token revocation

### Discovery

Clients can discover the authorization server configuration at:

```
GET /.well-known/oauth-authorization-server
```

Response:

```json theme={null}
{
  "issuer": "https://api.social-api.ai",
  "authorization_endpoint": "https://api.social-api.ai/oauth/authorize",
  "token_endpoint": "https://api.social-api.ai/oauth/token",
  "registration_endpoint": "https://api.social-api.ai/oauth/register",
  "revocation_endpoint": "https://api.social-api.ai/oauth/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["client_secret_post"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["social:all"]
}
```

### Step 1: Register a client

Register your application using dynamic client registration:

```bash theme={null}
curl -X POST https://api.social-api.ai/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My MCP Client",
    "redirect_uris": ["https://app.example.com/callback"]
  }'
```

Response (HTTP 201):

```json theme={null}
{
  "client_id": "a1b2c3d4e5f6...",
  "client_secret": "s3cr3t...",
  "client_id_issued_at": 1719000000,
  "client_secret_expires_at": 0,
  "client_name": "My MCP Client",
  "redirect_uris": ["https://app.example.com/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "client_secret_post"
}
```

<Warning>
  The `client_secret` is returned once. Store it securely. It cannot be retrieved again.
</Warning>

### Step 2: Authorize the user

Redirect the user to the authorization endpoint with PKCE parameters:

```
GET /oauth/authorize
  ?client_id=a1b2c3d4e5f6...
  &redirect_uri=https://app.example.com/callback
  &response_type=code
  &scope=social:all
  &state=random_csrf_value
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
```

The API validates the client and redirect URI, then redirects the user to the SocialAPI dashboard consent screen. After the user approves, they are redirected back to your `redirect_uri` with `?code=...&state=...`.

<Note>
  PKCE is required. The only supported challenge method is `S256`. Requests without a `code_challenge` are rejected with `400`.
</Note>

**Single scope.** The only supported scope is `social:all`, which grants full access to the user's account. This is the default if no scope is specified.

### Step 3: Exchange the code for tokens

```bash theme={null}
curl -X POST https://api.social-api.ai/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE_HERE" \
  -d "client_id=a1b2c3d4e5f6..." \
  -d "client_secret=s3cr3t..." \
  -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" \
  -d "redirect_uri=https://app.example.com/callback"
```

Response:

```json theme={null}
{
  "access_token": "eyJhbGciOiJFZERTQSIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_a1b2c3d4...",
  "scope": "social:all"
}
```

The access token is an EdDSA/Ed25519 signed JWT valid for **1 hour**. Use it as a Bearer token for all `/v1` endpoints.

<Note>
  Per RFC 6749 section 2.3.1, the token endpoint also accepts client credentials via HTTP Basic authentication. Send `Authorization: Basic base64(client_id:client_secret)` instead of including `client_id` and `client_secret` in the request body. Body parameters take precedence if both are present.
</Note>

### Step 4: Refresh the access token

When the access token expires, use the refresh token to get a new pair:

```bash theme={null}
curl -X POST https://api.social-api.ai/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=rt_a1b2c3d4..." \
  -d "client_id=a1b2c3d4e5f6..."
```

The response contains a new access token and a new refresh token. The old refresh token is invalidated (rotation is enforced). Refresh tokens expire after **30 days**.

### Revoking tokens

To revoke a refresh token (for example, on user logout):

```bash theme={null}
curl -X POST https://api.social-api.ai/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=rt_a1b2c3d4..." \
  -d "client_id=a1b2c3d4e5f6..."
```

Per RFC 7009, this endpoint always returns `200`, even if the token is already invalid.

### Supporting endpoints

| Endpoint                                  | Method | Purpose                                                                        |
| ----------------------------------------- | ------ | ------------------------------------------------------------------------------ |
| `/.well-known/oauth-authorization-server` | GET    | RFC 8414 metadata discovery                                                    |
| `/oauth/register`                         | POST   | RFC 7591 dynamic client registration                                           |
| `/oauth/authorize`                        | GET    | Authorization with PKCE                                                        |
| `/oauth/authorize/request`                | GET    | Fetch a pending authorization request by `request_id` (used by the consent UI) |
| `/oauth/token`                            | POST   | Code exchange and refresh token rotation                                       |
| `/oauth/revoke`                           | POST   | RFC 7009 token revocation                                                      |

### MCP server card

MCP clients can discover SocialAPI as a tool provider via the server card:

```
GET /.well-known/mcp/server-card.json
```

The protected resource metadata (RFC 9470) is available at:

```
GET /.well-known/oauth-protected-resource
GET /.well-known/oauth-protected-resource/mcp
```
