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 approved partner’s OAuth client. OAuth is available to both bookstores and Lit Orgs. Follow the OAuth integration guide to request credentials and build your connection flow.

  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 30 minutes before request time and ends one year later, clamped so the interval does not exceed 366 days. The earlier start keeps start-only events eligible; an event with an end time leaves the default snapshot when it ends, not 30 minutes 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. Omitted people and books preserve editor enrichment; an explicit array replaces that collection, and [] clears it. Lit Circle keeps workflow, presenting partners, recurrence, RSVP, featuring, and review data. Local archival always wins, and source silence or disconnect never mutates an Event.

  • Reuse books and people first

    POST and PUT accept optional people and books arrays, with at most eight entries each and one featured book. Identify participants by a saved id or name, with role set to author or presenter. Identify books by saved id, isbn, or exact title, with role set to featured or additional.

    Saved catalog records resolve first; missing books are looked up by ISBN or exact title. Existing profiles and confirmed book authorship stay intact. Unknown saved IDs and ambiguous saved records return field-specific 422 validation_failed; supply an explicit ID or ISBN to resolve ambiguity. A book the provider cannot match, or matches to several editions, is skipped instead: the Event saves with its other books and people, and the receipt's warnings names the skipped books[n] entry. Send a newer sourceUpdatedAt to retry it. Missing people become name-only profiles. Book credits never automatically add event participants, and an individual presenter is separate from the primary presenting organization. Every author, including co-authors, appears under Featured Authors; send moderators and interviewers as presenter, shown as In Conversation With.

    Book lookup failures return 502 unavailable or 503 unavailable. No event or catalog changes are saved on failure, and identical retries skip lookup.

  • 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

# Optional catalog fields can be added to the same event_payload JSON:
# "people": [{"id":"saved-person-id","role":"author"},{"name":"Example Presenter","role":"presenter"}]
# "books": [{"isbn":"9780140328721","role":"featured"}]
# Replace placeholder IDs with real saved IDs from Event GET responses.
# Saved records resolve first; missing books are looked up by ISBN or exact title. Unmatched books are skipped and listed in the receipt warnings.
# Omit people/books to preserve them; explicit arrays replace them and [] clears them.
# Book author credits alone do not imply participation.

# 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 genres and optional scalar fields clear.
# Omitted people/books are preserved; include arrays only to replace them.
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,
  };
}

// Optional fields in the same snapshot (replace placeholder IDs with real saved IDs):
// "people": [{"id":"saved-person-id","role":"author"},{"name":"Example Presenter","role":"presenter"}]
// "books": [{"isbn":"9780140328721","role":"featured"}]
// Saved records resolve first; missing books are looked up by ISBN or exact title. Unmatched books are skipped and listed in the receipt warnings.
// Omit people/books to preserve them; arrays replace them and [] clears them.
// Book author credits alone do not imply participation.
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,
    ];
}

// Optional fields in the same snapshot (replace placeholder IDs with real saved IDs):
// "people": [{"id":"saved-person-id","role":"author"},{"name":"Example Presenter","role":"presenter"}]
// "books": [{"isbn":"9780140328721","role":"featured"}]
// Saved records resolve first; missing books are looked up by ISBN or exact title. Unmatched books are skipped and listed in the receipt warnings.
// Omit people/books to preserve them; arrays replace them and [] clears them.
// Book author credits alone do not imply participation.
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,
    ];
}

// Optional fields in the same snapshot (replace placeholder IDs with real saved IDs):
// "people": [{"id":"saved-person-id","role":"author"},{"name":"Example Presenter","role":"presenter"}]
// "books": [{"isbn":"9780140328721","role":"featured"}]
// Saved records resolve first; missing books are looked up by ISBN or exact title. Unmatched books are skipped and listed in the receipt warnings.
// Omit people/books to preserve them; arrays replace them and [] clears them.
// Book author credits alone do not imply participation.
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