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.
-
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.
-
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.
-
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 -
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 formeta.window. If an Event disappears, remove it. Do not replace cached data after an error. -
Poll conditionally
Save the response
ETag, send it inIf-None-Match, and keep your current snapshot after304 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) andto(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_largemeans 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 forRetry-After. -
Retry writes deterministically
An identical POST or same-time identical PUT is safe and returns
unchanged. Resolveexternal_event_existsby 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
peopleandbookspreserve 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
peopleandbooksarrays, with at most eight entries each and one featured book. Identify participants by a savedidorname, withroleset toauthororpresenter. Identify books by savedid,isbn, or exacttitle, withroleset tofeaturedoradditional.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'swarningsnames the skippedbooks[n]entry. Send a newersourceUpdatedAtto 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. Everyauthor, including co-authors, appears under Featured Authors; send moderators and interviewers aspresenter, shown as In Conversation With.Book lookup failures return
502 unavailableor503 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
canonicalUrlas 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.