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 admin-registered OAuth client. OAuth is available to both bookstores and Lit Orgs.
-
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 at request time and ends one year 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. 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
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
# 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.