Lit Circle
Developer guideOAuth 2.0S256 PKCE

Connect your platform

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.

On this page

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.

  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 handlersNode.js
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

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. 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. Include the failing endpoint, response status, and the Events API request ID when present. Never include client secrets, authorization codes, verifiers, or access tokens.