Lit Circle
Events API Version 1 Read + write

Events API

Publish associated Lit Circle Events and connect an Event Source for an API-enabled bookstore or literary organization.

On this page

Get connected in four steps

API access starts with an enabled bookstore or Lit Org, one durable Event Source, and a server-side bearer.

  1. Enable subject access

    A Lit Circle admin enables Events API access for a bookstore or Lit Org. Access can be paused later without deleting credentials or grant history.

  2. Authorize the system

    An assigned manager can use a direct bookstore credential or authorize an admin-registered OAuth client. OAuth is available to both bookstores and Lit Orgs.

  3. Store the bearer securely

    Direct credentials and OAuth access tokens are opaque lc_live_... values. Store the value only in the external system's server secret manager.

    POST /api/v1/events
  4. Read or submit Events

    Read accepted taxonomy, POST each new External Event ID once, retain the returned Lit Circle Event ID, and use PUT for newer complete replacements.

    GET /api/v1/events

Integration contract

Treat every successful response as an authoritative snapshot for the returned window.

  • Replace snapshots

    After a successful 200, replace your previously stored Event set for meta.window. If an Event disappears, remove it. Do not replace cached data after an error.

  • Poll conditionally

    Save the response ETag, send it in If-None-Match, and keep your current snapshot after 304 Not Modified.

  • Choose a window

    The default begins at request time and ends one year later. To request historical data, supply both ISO-8601 from (inclusive) and to (exclusive); supplying only one is invalid, and the interval cannot exceed 366 days.

  • Respect bounds

    Snapshots are complete and unpaginated, with at most 500 Events. A 422 snapshot_too_large means you must request a narrower window; the API never silently truncates.

  • Back off safely

    Each bookstore or Lit Org receives 120 requests per minute across all its credentials and grants. After 429, wait for Retry-After.

  • Retry writes deterministically

    An identical POST or same-time identical PUT is safe and returns unchanged. Resolve external_event_exists by using PUT at the returned Event member URL; do not blindly retry validation, duplicate, or stale conflicts.

  • Know what the source owns

    PUT completely replaces source-managed fields and may overwrite local edits to them. Lit Circle keeps workflow, people, books, partners, recurrence, RSVP, featuring, and review data. Local archival always wins, and source silence or disconnect never mutates an Event.

  • White-labeling is allowed

    Visible “Powered by Lit Circle” attribution is optional for credentialed integrations. Preserve each Event's canonicalUrl as the authoritative Lit Circle link; provider metadata remains in the response.

Server-side examples

Each example keeps the bearer token in a server environment variable and demonstrates reads, POST/PUT submission, idempotent retries, recoverable conflicts, validation errors, and rate-limit backoff.

curl (server shell) bash
# Store these in your server's secret manager, not in source control.
export LIT_CIRCLE_API_TOKEN='lc_live_…'
export LIT_CIRCLE_ETAG=''

headers_file="$(mktemp)"
body_file="$(mktemp)"
trap 'rm -f "$headers_file" "$body_file"' EXIT

request_headers=(
  --header "Authorization: Bearer $LIT_CIRCLE_API_TOKEN"
  --header 'Accept: application/json'
)
if [ -n "$LIT_CIRCLE_ETAG" ]; then
  request_headers+=(--header "If-None-Match: $LIT_CIRCLE_ETAG")
fi

status="$(curl --silent --show-error \
  --dump-header "$headers_file" \
  --output "$body_file" \
  --write-out '%{http_code}' \
  "${request_headers[@]}" \
  'https://litcircle.org/api/v1/events?from=2026-09-01T00%3A00%3A00Z&to=2026-10-01T00%3A00%3A00Z')"

case "$status" in
  200)
    # Replace the stored snapshot only after this complete response succeeds.
    cat "$body_file"
    ;;
  304)
    echo 'Snapshot unchanged; keep the previously stored data.'
    ;;
  429)
    retry_after="$(awk 'tolower($1) == "retry-after:" { gsub("\r", "", $2); print $2 }' "$headers_file")"
    echo "Rate limited; retry after ${retry_after:-60} seconds." >&2
    exit 75
    ;;
  *)
    # Log the safe error code and requestId, never the bearer credential.
    cat "$body_file" >&2
    exit 1
    ;;
esac

# POST one complete snapshot. An identical retry returns 200 unchanged.
event_payload='{"externalEventId":"store-123","sourceUpdatedAt":"2026-08-21T14:00:00Z","sourceState":"scheduled","title":"Author Night","schedule":{"startsAt":"2026-09-18T19:00:00-05:00"},"eventTypes":["Author Event"],"ageRating":"allAges","admission":{"isFree":true}}'
write_status="$(curl --silent --show-error --output "$body_file" --write-out '%{http_code}'   --header "Authorization: Bearer $LIT_CIRCLE_API_TOKEN"   --header 'Content-Type: application/json' --data "$event_payload"   'https://litcircle.org/api/v1/events')"
case "$write_status" in
  200|201) cat "$body_file" ;; # retain data.id for PUT
  409|422) cat "$body_file" >&2 ;; # inspect safe code/details; do not blindly retry
  429) echo 'Rate limited; honor Retry-After.' >&2 ;;
  *) cat "$body_file" >&2; exit 1 ;;
esac

# PUT a newer complete snapshot; omitted optional source-managed fields clear.
curl --request PUT --header "Authorization: Bearer $LIT_CIRCLE_API_TOKEN"   --header 'Content-Type: application/json' --data "$event_payload"   'https://litcircle.org/api/v1/events/RETAINED_LIT_CIRCLE_EVENT_ID'

# DELETE is permanent and succeeds only for an API-created Event in this bookstore.
curl --request DELETE --header "Authorization: Bearer $LIT_CIRCLE_API_TOKEN"   'https://litcircle.org/api/v1/events/RETAINED_LIT_CIRCLE_EVENT_ID'
Node / JavaScript javascript
export async function fetchLitCircleEvents({ etag, from, to } = {}) {
  const token = process.env.LIT_CIRCLE_API_TOKEN;
  if (!token) throw new Error("LIT_CIRCLE_API_TOKEN is not configured");

  if ((from == null) !== (to == null)) {
    throw new Error("from and to must be supplied together");
  }

  const url = new URL("https://litcircle.org/api/v1/events");
  if (from != null && to != null) {
    url.searchParams.set("from", from);
    url.searchParams.set("to", to);
  }

  const headers = {
    Accept: "application/json",
    Authorization: `Bearer ${token}`,
  };
  if (etag) headers["If-None-Match"] = etag;

  const response = await fetch(url, { headers });
  if (response.status === 304) return { unchanged: true };
  if (response.status === 429) {
    const retryAfter = Number(response.headers.get("Retry-After") ?? "60");
    return { rateLimited: true, retryAfter };
  }

  const payload = await response.json();
  if (!response.ok) {
    throw new Error(
      `Lit Circle ${payload.error.code} (request ${payload.error.requestId})`,
    );
  }

  return {
    unchanged: false,
    etag: response.headers.get("ETag"),
    // This is the complete snapshot for payload.meta.window. Replace that
    // window's previous data only after the 200 response has been parsed.
    snapshot: payload,
  };
}

export async function submitLitCircleEvent({ eventId, snapshot }) {
  const token = process.env.LIT_CIRCLE_API_TOKEN;
  if (!token) throw new Error("LIT_CIRCLE_API_TOKEN is not configured");
  const url = eventId
    ? `https://litcircle.org/api/v1/events/${encodeURIComponent(eventId)}`
    : "https://litcircle.org/api/v1/events";
  const response = await fetch(url, {
    method: eventId ? "PUT" : "POST",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify(snapshot),
  });
  const payload = await response.json();
  if (response.status === 429) {
    return { retryAfter: Number(response.headers.get("Retry-After") ?? "60") };
  }
  if (response.status === 409 || response.status === 422) {
    return { conflict: payload.error.code, details: payload.error.details, requestId: payload.error.requestId };
  }
  if (!response.ok) throw new Error(`Lit Circle ${payload.error.code} (request ${payload.error.requestId})`);
  return payload; // Retain data.id. A 200 unchanged receipt is successful.
}

export async function deleteLitCircleEvent(eventId) {
  const token = process.env.LIT_CIRCLE_API_TOKEN;
  const response = await fetch(
    `https://litcircle.org/api/v1/events/${encodeURIComponent(eventId)}`,
    { method: "DELETE", headers: { Authorization: `Bearer ${token}` } },
  );
  if (response.status === 204) return true;
  const payload = await response.json();
  throw new Error(`Lit Circle ${payload.error.code} (request ${payload.error.requestId})`);
}
PHP php
<?php
function fetch_lit_circle_events(?string $etag = null): array {
    $token = getenv('LIT_CIRCLE_API_TOKEN');
    if (!$token) {
        throw new RuntimeException('LIT_CIRCLE_API_TOKEN is not configured');
    }

    $headers = [
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
    ];
    if ($etag) $headers[] = 'If-None-Match: ' . $etag;

    $responseHeaders = [];
    $curl = curl_init('https://litcircle.org/api/v1/events');
    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_HEADERFUNCTION => function ($curl, $line) use (&$responseHeaders) {
            $parts = explode(':', $line, 2);
            if (count($parts) === 2) {
                $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
            }
            return strlen($line);
        },
    ]);

    $body = curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    if ($body === false) throw new RuntimeException(curl_error($curl));
    curl_close($curl);

    if ($status === 304) return ['unchanged' => true];
    if ($status === 429) return [
        'rateLimited' => true,
        'retryAfter' => (int)($responseHeaders['retry-after'] ?? 60),
    ];

    $payload = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
    if ($status < 200 || $status >= 300) {
        // Report only the safe code/request ID. Never log $token or $headers.
        throw new RuntimeException(sprintf(
            'Lit Circle %s (request %s)',
            $payload['error']['code'],
            $payload['error']['requestId'],
        ));
    }

    return [
        'unchanged' => false,
        'etag' => $responseHeaders['etag'] ?? null,
        'snapshot' => $payload,
    ];
}

function submit_lit_circle_event(array $snapshot, ?string $eventId = null): array {
    $token = getenv('LIT_CIRCLE_API_TOKEN');
    $url = 'https://litcircle.org/api/v1/events' . ($eventId ? '/' . rawurlencode($eventId) : '');
    $curl = curl_init($url);
    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => $eventId ? 'PUT' : 'POST',
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode($snapshot, JSON_THROW_ON_ERROR),
    ]);
    $body = curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    if ($body === false) throw new RuntimeException(curl_error($curl));
    curl_close($curl);
    $payload = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
    if (in_array($status, [409, 422, 429], true)) return ['status' => $status, 'error' => $payload['error']];
    if ($status < 200 || $status >= 300) throw new RuntimeException('Lit Circle write failed');
    return $payload; // Retain data.id; 200 unchanged is successful.
}

function delete_lit_circle_event(string $eventId): bool {
    $token = getenv('LIT_CIRCLE_API_TOKEN');
    $curl = curl_init('https://litcircle.org/api/v1/events/' . rawurlencode($eventId));
    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => 'DELETE',
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
    ]);
    curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);
    if ($status !== 204) throw new RuntimeException('Lit Circle delete failed');
    return true;
}
WordPress (server-side plugin or theme) php
<?php
function my_store_lit_circle_events(?string $etag = null): array {
    // Configure this as a server environment secret, never a WP option that
    // is exposed to browser code or checked into the theme.
    $token = getenv('LIT_CIRCLE_API_TOKEN');
    if (!$token) return ['error' => 'Lit Circle API is not configured'];

    $headers = [
        'Accept' => 'application/json',
        'Authorization' => 'Bearer ' . $token,
    ];
    if ($etag) $headers['If-None-Match'] = $etag;

    $response = wp_remote_get('https://litcircle.org/api/v1/events', [
        'headers' => $headers,
        'timeout' => 15,
    ]);
    if (is_wp_error($response)) return ['error' => $response->get_error_message()];

    $status = wp_remote_retrieve_response_code($response);
    if ($status === 304) return ['unchanged' => true];
    if ($status === 429) return [
        'rateLimited' => true,
        'retryAfter' => (int)(wp_remote_retrieve_header($response, 'retry-after') ?: 60),
    ];

    $payload = json_decode(wp_remote_retrieve_body($response), true);
    if ($status < 200 || $status >= 300) {
        // Send these safe values to support; do not include $token or $headers.
        return [
            'error' => $payload['error']['code'] ?? 'invalid_response',
            'requestId' => $payload['error']['requestId'] ?? null,
        ];
    }

    return [
        'unchanged' => false,
        'etag' => wp_remote_retrieve_header($response, 'etag'),
        // Atomically replace the cached data for $payload['meta']['window'].
        'snapshot' => $payload,
    ];
}

function my_store_submit_lit_circle_event(array $snapshot, ?string $eventId = null): array {
    $token = getenv('LIT_CIRCLE_API_TOKEN');
    $url = 'https://litcircle.org/api/v1/events' . ($eventId ? '/' . rawurlencode($eventId) : '');
    $response = wp_remote_request($url, [
        'method' => $eventId ? 'PUT' : 'POST',
        'headers' => ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
        'body' => wp_json_encode($snapshot),
        'timeout' => 15,
    ]);
    if (is_wp_error($response)) return ['error' => $response->get_error_message()];
    $status = wp_remote_retrieve_response_code($response);
    $payload = json_decode(wp_remote_retrieve_body($response), true);
    if (in_array($status, [409, 422, 429], true)) return ['status' => $status, 'error' => $payload['error']];
    if ($status < 200 || $status >= 300) return ['error' => 'Lit Circle write failed'];
    return $payload; // Retain data.id; identical retries may return unchanged.
}

function my_store_delete_lit_circle_event(string $eventId): bool {
    $token = getenv('LIT_CIRCLE_API_TOKEN');
    $response = wp_remote_request(
        'https://litcircle.org/api/v1/events/' . rawurlencode($eventId),
        [
            'method' => 'DELETE',
            'headers' => ['Authorization' => 'Bearer ' . $token],
            'timeout' => 15,
        ],
    );
    return !is_wp_error($response) && wp_remote_retrieve_response_code($response) === 204;
}

Test with Postman

Import the collection and environment template to exercise every v1 endpoint without putting a bearer into this browser page.

Credential-safe by default

Add your direct credential or OAuth access token only to the imported local environment. The template contains no secret, read requests capture useful values such as ETags and Event IDs, and production POST/PUT requests remain blocked until you explicitly set allowWrites to true.

Do not sync or export an environment after adding a bearer. Prefer a dedicated, revocable connection for testing.

Compatibility and retirement

Additive within v1

Changes within /api/v1 are additive. Consumers must ignore fields they do not recognize. Removing a field, changing its type or meaning, or changing Event eligibility semantics requires a new major API version.

At least 90 days of overlap

Once a version has real consumers, a superseded version remains available for at least 90 days. Retirement is announced with Deprecation, Sunset, and Link response headers plus manager-facing notices.

OpenAPI reference

Explore the complete response shapes, parameters, status codes, and error contract.

Open OpenAPI 3.1 JSON