# Connect your platform

> Markdown edition of https://litcircle.org/api/docs/oauth for agents and text clients. Agents can help a developer build this flow, but a person must approve partner access and each bookstore or Lit Org connection in a browser. After connecting, use the [Events API reference](<https://litcircle.org/api/docs.md>).

Add a “Connect to Lit Circle” button to your event platform. A bookstore or literary organization manager approves access, and your server receives a token for that entity’s Events API.

[Request partner access](<https://litcircle.org/account/integrations>) [Build the connection](<https://litcircle.org/api/docs/oauth#connection-flow>)

Who does what?

**Your developer** registers the application, stores its credentials, and builds the connection button, callback handler, and event sync.

**The bookstore manager** starts in your platform, signs in to Lit Circle, chooses their bookstore, and approves access. They do not request developer access or generate application credentials.

Your platform must implement the connection and event sync before bookstores can use it. OAuth availability in Lit Circle alone does not move events.

**A Lit Circle admin** approves your partner request and enables OAuth for each eligible bookstore or Lit Org. Enabling OAuth makes consent available; it does not create a connection.

## Get your application credentials

Complete this once for your platform. Each customer authorizes their own connection afterward.

1. **Create and verify your account**

   Sign up for Lit Circle, verify your email, and open [Account → Developer integrations](<https://litcircle.org/account/integrations>).

2. **Submit your platform for approval**

   Enter your platform name and exact HTTPS callback origins, such as `https://platform.example.com`. Request production and staging origins separately. A Lit Circle admin verifies your identity and control of these origins before approving.

3. **Generate credentials after approval**

   Return to Developer integrations, enter an application name and the complete callback URL, such as `https://platform.example.com/oauth/lit-circle/callback`, and confirm your Lit Circle password. Save the generated client ID and one-time client secret in your server’s secret manager.

Register one to ten exact HTTPS callback URLs on approved origins. Wildcards, fragments, and URLs containing user information are rejected. The URL used for authorization and token exchange must match a registered URL exactly. Up to ten integrations may be active per approved partner.

Partner approval gives you credential-management access. Each bookstore or Lit Org still needs OAuth enabled by an admin and consent from an assigned manager. Browser-only applications are not supported.

## Connect a bookstore in four steps

Use the authorization-code flow with client-secret authentication and S256 PKCE. Start the connection from a signed-in customer administrator in your platform.

1. **Send the manager to Lit Circle**

   Create a cryptographically random `state` and a fresh PKCE `code_verifier` for every attempt. Store both in the initiating customer’s server-side session. Redirect the browser to `GET /oauth/authorize` with these parameters:

   ```
   https://litcircle.org/oauth/authorize
     ?response_type=code
     &client_id=YOUR_CLIENT_ID
     &redirect_uri=https%3A%2F%2Fplatform.example.com%2Foauth%2Flit-circle%2Fcallback
     &state=RANDOM_VALUE_BOUND_TO_YOUR_CUSTOMER_SESSION
     &code_challenge=BASE64URL_SHA256_OF_YOUR_VERIFIER
     &code_challenge_method=S256
   ```

   The example is wrapped for readability; send one URL with percent-encoded query values. The verifier must be 43–128 characters from `A–Z a–z 0–9 - . _ ~`. The challenge is the unpadded base64url SHA-256 digest of the verifier. Send `code_challenge_method=S256`; `plain` is rejected. Do not send the verifier or client secret in the authorization URL.

2. **Handle the approval callback**

   Lit Circle asks the manager to sign in, choose an eligible bookstore or Lit Org, and approve or cancel. Approval redirects to your callback with `code` and `state`. Verify that state matches the initiating session before linking anything. Handle `error=access_denied` as a cancellation. Codes expire after 10 minutes and can be exchanged once.

   ```
   https://platform.example.com/oauth/lit-circle/callback?code=lc_code_...&state=YOUR_ORIGINAL_STATE
   ```

3. **Exchange the code on your server**

   POST the code, original verifier, client credentials, and exact callback URL as `application/x-www-form-urlencoded`. These shell variables refer to values stored on your server:

   ```
   curl --request POST https://litcircle.org/oauth/token \
     --data-urlencode 'grant_type=authorization_code' \
     --data-urlencode "client_id=$LIT_CIRCLE_CLIENT_ID" \
     --data-urlencode "client_secret=$LIT_CIRCLE_CLIENT_SECRET" \
     --data-urlencode "code=$LIT_CIRCLE_AUTHORIZATION_CODE" \
     --data-urlencode "redirect_uri=$LIT_CIRCLE_CALLBACK_URL" \
     --data-urlencode "code_verifier=$LIT_CIRCLE_CODE_VERIFIER"
   ```

   The successful response contains:

   ```
   {"access_token":"lc_live_...","token_type":"bearer"}
   ```

   HTTP Basic client authentication is also accepted. Tokens are long-lived and opaque; there is no refresh-token grant or `expires_in` value. Never expose the token to browser JavaScript or log credentials.

4. **Confirm the entity and save the connection**

   Call `GET /api/v1/events` with `Authorization: Bearer YOUR_ACCESS_TOKEN`. The response’s `meta.subject` contains the authorized entity’s ID, name, and type. Confirm that selection with your customer, then store the token for that customer and entity. Use a separate connection for each bookstore or Lit Org; never attach a callback to a customer based only on a URL parameter.

## Node.js server example

Adapt these functions to your platform’s authenticated routes and durable server-side session store. Configure the three environment variables with your generated credentials and registered callback URL.

Your start and callback routes must enforce customer-administrator access. Derive `customerId` from the authenticated server context, preserve it through the callback, and atomically claim the attempt before exchanging the code. Render a safe connection error on failure, without logging codes or tokens. These functions illustrate the protocol; your framework supplies session persistence and token storage.

**Authorization and callback handlers**

```
import { createHash, randomBytes } from "node:crypto";

const base = "https://litcircle.org";
const clientId = process.env.LIT_CIRCLE_CLIENT_ID;
const clientSecret = process.env.LIT_CIRCLE_CLIENT_SECRET;
const redirectUri = process.env.LIT_CIRCLE_CALLBACK_URL;
if (!clientId || !clientSecret || !redirectUri) {
  throw new Error("Configure Lit Circle credentials and the exact callback URL.");
}

// Call these only for a signed-in customer administrator.
// session is your framework's SERVER-SIDE session, never browser storage.
export function beginAuthorization(session, customerId) {
  const state = randomBytes(32).toString("base64url");
  const verifier = randomBytes(32).toString("base64url");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  session.litCircleOAuth = { state, verifier, customerId, startedAt: Date.now() };
  // Persist the session before redirecting the browser to this URL.
  const url = new URL("/oauth/authorize", base);
  url.search = new URLSearchParams({
    response_type: "code", client_id: clientId, redirect_uri: redirectUri,
    state, code_challenge: challenge, code_challenge_method: "S256",
  }).toString();
  return url.toString();
}

export async function completeAuthorization(callbackUrl, session, customerId) {
  const url = new URL(callbackUrl);
  const pending = session.litCircleOAuth;
  if (!pending || pending.customerId !== customerId || !url.searchParams.get("state") ||
      url.searchParams.get("state") !== pending.state ||
      Date.now() - pending.startedAt > 10 * 60 * 1000) {
    throw new Error("Connection attempt expired or state did not match.");
  }
  delete session.litCircleOAuth;
  // Persist removal before continuing; only one callback may claim this attempt.
  if (url.searchParams.has("error")) {
    throw new Error("Connection was declined or could not be completed.");
  }
  const code = url.searchParams.get("code");
  if (!code) throw new Error("Missing authorization code.");
  const response = await fetch(new URL("/oauth/token", base), {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code", client_id: clientId,
      client_secret: clientSecret, code, redirect_uri: redirectUri,
      code_verifier: pending.verifier,
    }),
  });
  if (!response.ok) throw new Error("Token exchange failed; reconnect.");
  const token = await response.json();
  if (token.token_type !== "bearer" || !token.access_token) {
    throw new Error("Unexpected token response.");
  }
  const events = await fetch(new URL("/api/v1/events", base), {
    headers: { Authorization: "Bearer " + token.access_token },
  });
  if (!events.ok) throw new Error("Could not identify the authorized entity.");
  const { meta } = await events.json();
  // Confirm meta.subject with the customer before linking the connection.
  // Store the token in server secret storage for this customer's entity.
  return { accessToken: token.access_token, subject: meta.subject };
}
```

## Use the Events API

The customer’s token is your server’s credential for that entity.

```
curl https://litcircle.org/api/v1/events \
  --header "Authorization: Bearer $LIT_CIRCLE_ACCESS_TOKEN"
```

Read public associated events with GET. Create events with POST, retain each returned Lit Circle event ID, update complete source-managed snapshots with PUT, and explicitly remove owned events with DELETE. Writes are restricted to the entity’s Event Source; you cannot edit unrelated or manually created events.

V1 has no granular OAuth scopes: a token grants the entity-scoped read and write operations the API exposes. Do not request `scope` or `offline_access`. There are no webhooks or incremental-sync endpoints. Follow the API’s snapshot, taxonomy, idempotency, and rate-limit rules.

[Read the Events API reference](<https://litcircle.org/api/docs>)

## Disconnect and credential changes

Add a disconnect action in your platform. Revoke the customer’s token on your server, then remove the stored connection and stop syncing:

```
curl --request POST https://litcircle.org/oauth/revoke \
  --data-urlencode "client_id=$LIT_CIRCLE_CLIENT_ID" \
  --data-urlencode "client_secret=$LIT_CIRCLE_CLIENT_SECRET" \
  --data-urlencode "token=$LIT_CIRCLE_ACCESS_TOKEN"
```

For valid client credentials, revocation returns `200` even if the token is unknown or already revoked. Disconnecting does not delete events already sent to Lit Circle.

- **Callback changes**

  Edit callbacks in [Developer integrations](<https://litcircle.org/account/integrations>). Changes invalidate pending authorization codes, while issued tokens stay connected.

- **Secret rotation**

  Rotation immediately invalidates the old secret, pending codes, and all issued tokens for that integration. Update your server’s secret and ask every affected customer to reconnect.

- **Revoked access**

  Client or partner revocation stops access. Disabling a subject’s OAuth capability also stops its API access. Reapproving a partner never restores revoked clients; generate a new integration when needed.

## Troubleshooting

Show customers a useful error and offer a fresh connection attempt when appropriate.

- **No eligible bookstore**

  The manager must be actively assigned to the entity, and a Lit Circle admin must enable its OAuth capability. Archived entities cannot authorize a connection. Your developer’s partner approval does not supply manager access.

- **Callback mismatch**

  Compare the complete URL, including path, query, and trailing slash, with the registered callback. An unregistered callback produces a local Lit Circle error; Lit Circle never redirects a code to it.

- **State mismatch**

  Reject the callback. Do not exchange the code or bind it to a customer. Preserve the original server-side session through the redirect and start a new attempt.

- **`invalid_request`**

  Check authorization parameters, the S256 challenge, and form encoding. Missing or malformed PKCE is rejected. Trusted callbacks receive protocol errors through `error` and `error_description`.

- **`invalid_client`**

  A token-endpoint `401` means client authentication failed. Check the client ID and current secret and whether the client or owning partner was revoked.

- **`invalid_grant`**

  A token-endpoint `400` can mean an expired, used, or invalid code; mismatched callback; wrong or missing verifier; or access changed since approval. Start a fresh authorization attempt. A wrong verifier does not consume the code.

- **API `401` or `403`**

  Stop syncing and check whether the token, client, partner, entity capability, or Event Source was revoked or disabled. Offer reconnect when the stored credential is invalid; ask a Lit Circle admin to restore the entity’s OAuth capability when it is disabled.

- **API `429`**

  Wait for `Retry-After` before retrying. Rate limits are shared across the entity’s credentials and grants.

For implementation help, [contact Lit Circle](<https://litcircle.org/contact>). Include the failing endpoint, response status, and the Events API request ID when present. Never include client secrets, authorization codes, verifiers, or access tokens.