# Events API

> Markdown edition of https://litcircle.org/api/docs for agents and text clients. API version 1.0.0. The machine-readable contract is the [OpenAPI 3.1 document](<https://litcircle.org/api/v1/openapi.json>). To obtain a bearer token through a partner platform, read the [OAuth integration guide](<https://litcircle.org/api/docs/oauth.md>). Every endpoint requires a server-side bearer credential; never place one in browser code, a URL, or a prompt transcript.

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

[Start integrating](<https://litcircle.org/api/docs#quick-start>) [Download OpenAPI JSON](<https://litcircle.org/api/v1/openapi.json>)

Credential safety

**Server-to-server only.** Keep each direct credential or OAuth access token in your server's secret manager and send it only in the `Authorization` header—never in browser JavaScript, a public repository, a URL, or browser storage.

Bookstores that need a browser-only integration can use the public [events iframe](<https://litcircle.org/embed/events/chicago/your-bookstore-slug>); it does not require a secret.

## 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](<https://litcircle.org/api/docs/oauth>) 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.

[Download collection](<https://litcircle.org/api/docs/postman-collection>) [Download environment template](<https://litcircle.org/api/docs/postman-environment>)

## 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](<https://litcircle.org/api/v1/openapi.json>)

## Endpoint reference

Generated from the [OpenAPI 3.1 document](<https://litcircle.org/api/v1/openapi.json>). Fetch that JSON for complete request and response schemas, constraints, and examples.

Base URL: `https://litcircle.org`

Authentication `bookstoreBearer` (HTTP bearer): An opaque API bearer issued directly to a bookstore or through OAuth to a bookstore or literary organization. Keep it in server-side secret storage and send it only in the Authorization header.

### `GET /api/v1/events`

Get an authoritative Event Feed Snapshot.

Returns all matching Public Events associated with the credential's bookstore or literary organization for the resolved Event Feed Window. Events are selected by interval overlap; custom `from` and `to` values must be supplied together, `from` is inclusive, and `to` is exclusive. The response is unpaginated and never silently truncated.

- Operation ID: `listBookstoreEvents`
- Authentication: bookstoreBearer

Parameters:

- `from` (query, optional, string (date-time)): Inclusive ISO-8601 instant. `from` and `to` must be supplied together; omit both to use the default window beginning 30 minutes before request time. That earlier start applies to start-only events. Events with an end time overlap until the request time, and the default end is clamped so the interval does not exceed 366 days. Example: `2026-01-01T00:00:00Z`.
- `to` (query, optional, string (date-time)): Exclusive ISO-8601 instant. `from` and `to` must be supplied together; omit both to use the default window ending one year after request time. Example: `2026-12-31T00:00:00Z`.
- `If-None-Match` (header, optional, string): A prior ETag. An unchanged snapshot returns 304 without a body.

Responses:

- `200`: A complete Event Feed Snapshot containing at most 500 Events. Body: EventsResponse.
- `304`: The Event Feed Snapshot has not changed. The response has no body.
- `400`: A query parameter is malformed. Body: ErrorResponse. Error code: `invalid_request`.
- `401`: The bearer credential is missing, malformed, revoked, or invalid. Body: ErrorResponse. Error code: `unauthorized`.
- `403`: The credential is valid but API access is paused or its bookstore or literary organization is archived. Body: ErrorResponse. Error code: `forbidden`.
- `405`: The collection supports GET and POST. Body: ErrorResponse. Error code: `invalid_request`.
- `422`: The Event Feed Window is invalid or exceeds 366 days, or more than 500 Events match it. Narrow the window and retry. Body: ErrorResponse. Error codes: `invalid_window`, `snapshot_too_large`.
- `429`: The API subject exceeded 120 requests in the current one-minute window. Body: ErrorResponse. Error code: `rate_limited`.
- `500`: An unexpected failure occurred. The request ID can be shared with support. Body: ErrorResponse. Error code: `internal_error`.
- `503`: A required service is temporarily unavailable. Body: ErrorResponse. Error code: `unavailable`.

### `POST /api/v1/events`

Submit one new source-managed Event snapshot.

Creates one Event for the active Event Source. Identical retries are unchanged; changed reuse of an External Event ID conflicts and must continue with PUT.

- Operation ID: `createSourceManagedEvent`
- Authentication: bookstoreBearer

Request body (required):

- `application/json`: EventWriteInput

Responses:

- `200`: An identical retry was accepted as unchanged. Body: EventWriteResponse.
- `201`: The source-managed Event and provenance were created atomically. Body: EventWriteResponse.
- `400`: The JSON body is malformed. Body: ErrorResponse. Error code: `invalid_request`.
- `401`: The bearer credential is missing, malformed, revoked, or invalid. Body: ErrorResponse. Error code: `unauthorized`.
- `403`: The credential is valid but API access is paused or its bookstore or literary organization is archived. Body: ErrorResponse. Error code: `forbidden`.
- `405`: The collection supports GET and POST. Body: ErrorResponse. Error code: `invalid_request`.
- `409`: Returns source_not_configured when no source is active, external_event_exists for changed identity reuse, or potential_duplicate for a title/start match. Body: ErrorResponse.
- `422`: The strict Event Submission failed validation. Body: ErrorResponse. Error code: `validation_failed`.
- `429`: The API subject exceeded 120 requests in the current one-minute window. Body: ErrorResponse. Error code: `rate_limited`.
- `500`: An unexpected failure occurred. The request ID can be shared with support. Body: ErrorResponse. Error code: `internal_error`.
- `502`: Book lookup failed; no changes were saved. Body: ErrorResponse. Error code: `unavailable`.
- `503`: A required service is temporarily unavailable. Body: ErrorResponse. Error code: `unavailable`.

### `GET /api/v1/events/{eventId}`

Get one associated Public Event.

Returns the same complete Event representation as the list endpoint. Missing, hidden, and out-of-scope Events all return the same 404 response.

- Operation ID: `getBookstoreEvent`
- Authentication: bookstoreBearer

Parameters:

- `eventId` (path, required, string): The public Event ID.
- `If-None-Match` (header, optional, string): A prior ETag. An unchanged Event returns 304 without a body.

Responses:

- `200`: The associated Public Event. Body: EventResponse.
- `304`: The Event representation has not changed. The response has no body.
- `401`: The bearer credential is missing, malformed, revoked, or invalid. Body: ErrorResponse. Error code: `unauthorized`.
- `403`: The credential is valid but API access is paused or its bookstore or literary organization is archived. Body: ErrorResponse. Error code: `forbidden`.
- `404`: The Event is missing, not Public, or unrelated to the authenticated API subject. Body: ErrorResponse. Error code: `not_found`.
- `405`: The member supports GET, PUT, and DELETE. Body: ErrorResponse. Error code: `invalid_request`.
- `429`: The API subject exceeded 120 requests in the current one-minute window. Body: ErrorResponse. Error code: `rate_limited`.
- `500`: An unexpected failure occurred. The request ID can be shared with support. Body: ErrorResponse. Error code: `internal_error`.
- `503`: A required service is temporarily unavailable. Body: ErrorResponse. Error code: `unavailable`.

### `PUT /api/v1/events/{eventId}`

Replace one source-managed Event snapshot.

Completely replaces the source-managed field set for an Event linked to the authenticated bookstore or literary organization's active source. Lit Circle-owned workflow and omitted enrichment survive. Explicit people/books arrays replace their collection after catalog-first resolution.

- Operation ID: `replaceSourceManagedEvent`
- Authentication: bookstoreBearer

Parameters:

- `eventId` (path, required, string)

Request body (required):

- `application/json`: EventWriteInput

Responses:

- `200`: The snapshot was updated or was an identical retry. Body: EventWriteResponse.
- `400`: The JSON body is malformed. Body: ErrorResponse. Error code: `invalid_request`.
- `401`: The bearer credential is missing, malformed, revoked, or invalid. Body: ErrorResponse. Error code: `unauthorized`.
- `403`: The credential is valid but API access is paused or its bookstore or literary organization is archived. Body: ErrorResponse. Error code: `forbidden`.
- `404`: The Event is missing or outside the active source's write scope. Body: ErrorResponse. Error code: `not_found`.
- `405`: The member supports GET, PUT, and DELETE. Body: ErrorResponse. Error code: `invalid_request`.
- `409`: The submission is older or differs at the same source update time. Body: ErrorResponse. Error code: `stale_submission`.
- `422`: The strict Event Submission failed validation. Body: ErrorResponse. Error code: `validation_failed`.
- `429`: The API subject exceeded 120 requests in the current one-minute window. Body: ErrorResponse. Error code: `rate_limited`.
- `500`: An unexpected failure occurred. The request ID can be shared with support. Body: ErrorResponse. Error code: `internal_error`.
- `502`: Book lookup failed; no changes were saved. Body: ErrorResponse. Error code: `unavailable`.
- `503`: A required service is temporarily unavailable. Body: ErrorResponse. Error code: `unavailable`.

### `DELETE /api/v1/events/{eventId}`

Delete one API-created Event.

Permanently deletes an Event only when it has Event Source provenance and belongs to the authenticated bookstore or literary organization. Manual, cross-subject, and missing Events all return the same 404 response.

- Operation ID: `deleteSourceManagedEvent`
- Authentication: bookstoreBearer

Parameters:

- `eventId` (path, required, string)

Responses:

- `204`: The API-created Event was permanently deleted. The response has no body.
- `401`: The bearer credential is missing, malformed, revoked, or invalid. Body: ErrorResponse. Error code: `unauthorized`.
- `403`: The credential is valid but API access is paused or its bookstore or literary organization is archived. Body: ErrorResponse. Error code: `forbidden`.
- `404`: The Event is missing, was not created through the API, or belongs to another subject. Body: ErrorResponse. Error code: `not_found`.
- `405`: The member supports GET, PUT, and DELETE. Body: ErrorResponse. Error code: `invalid_request`.
- `429`: The API subject exceeded 120 requests in the current one-minute window. Body: ErrorResponse. Error code: `rate_limited`.
- `500`: An unexpected failure occurred. The request ID can be shared with support. Body: ErrorResponse. Error code: `internal_error`.
- `503`: A required service is temporarily unavailable. Body: ErrorResponse. Error code: `unavailable`.

### `GET /api/v1/event-types`

List current Event Type labels accepted by writes.

- Operation ID: `listAcceptedEventTypes`
- Authentication: bookstoreBearer

Responses:

- `200`: Current labels. Body: TaxonomyResponse.
- `401`: The bearer credential is missing, malformed, revoked, or invalid. Body: ErrorResponse. Error code: `unauthorized`.
- `403`: The credential is valid but API access is paused or its bookstore or literary organization is archived. Body: ErrorResponse. Error code: `forbidden`.
- `405`: The endpoint only accepts GET requests. Body: ErrorResponse. Error code: `invalid_request`.
- `429`: The API subject exceeded 120 requests in the current one-minute window. Body: ErrorResponse. Error code: `rate_limited`.
- `500`: An unexpected failure occurred. The request ID can be shared with support. Body: ErrorResponse. Error code: `internal_error`.
- `503`: A required service is temporarily unavailable. Body: ErrorResponse. Error code: `unavailable`.

### `GET /api/v1/genres`

List current Genre labels accepted by writes.

- Operation ID: `listAcceptedGenres`
- Authentication: bookstoreBearer

Responses:

- `200`: Current labels. Body: TaxonomyResponse.
- `401`: The bearer credential is missing, malformed, revoked, or invalid. Body: ErrorResponse. Error code: `unauthorized`.
- `403`: The credential is valid but API access is paused or its bookstore or literary organization is archived. Body: ErrorResponse. Error code: `forbidden`.
- `405`: The endpoint only accepts GET requests. Body: ErrorResponse. Error code: `invalid_request`.
- `429`: The API subject exceeded 120 requests in the current one-minute window. Body: ErrorResponse. Error code: `rate_limited`.
- `500`: An unexpected failure occurred. The request ID can be shared with support. Body: ErrorResponse. Error code: `internal_error`.
- `503`: A required service is temporarily unavailable. Body: ErrorResponse. Error code: `unavailable`.

### `GET /api/v1/neighborhoods`

List Neighborhoods in the authenticated subject's Area.

- Operation ID: `listAvailableNeighborhoods`
- Authentication: bookstoreBearer

Responses:

- `200`: Neighborhood IDs accepted by off-site Event writes. Body: NeighborhoodsResponse.
- `401`: The bearer credential is missing, malformed, revoked, or invalid. Body: ErrorResponse. Error code: `unauthorized`.
- `403`: The credential is valid but API access is paused or its bookstore or literary organization is archived. Body: ErrorResponse. Error code: `forbidden`.
- `405`: The endpoint only accepts GET requests. Body: ErrorResponse. Error code: `invalid_request`.
- `429`: The API subject exceeded 120 requests in the current one-minute window. Body: ErrorResponse. Error code: `rate_limited`.
- `500`: An unexpected failure occurred. The request ID can be shared with support. Body: ErrorResponse. Error code: `internal_error`.
- `503`: A required service is temporarily unavailable. Body: ErrorResponse. Error code: `unavailable`.

## Schemas

Optional properties end in `?`.

- `WriteInstant` (string). Instant with an explicit UTC offset. Accepts RFC 3339 (`Z` or `±HH:MM`) and the colonless `±HHMM` spelling.
- `TaxonomyResponse` (object). Properties: `data`: array of string; `meta`: object.
- `NeighborhoodsResponse` (object). Properties: `data`: array of object; `meta`: object.
- `EventWriteInput` (object). Properties: `people?`: array of object; `books?`: array of object; `externalEventId`: string; `sourceUpdatedAt`: WriteInstant; `sourceState`: "scheduled" | "cancelled"; `sourceUrl?`: string (uri) | null; `title`: string; `description?`: object | null; `schedule`: object; `eventTypes`: array of string; `genres?`: array of string; `ageRating`: "allAges" | "kids" | "teens" | "adults"; `venue?`: object; `admission`: object; `image?`: object | null.
- `EventWriteResponse` (object). Properties: `data`: object; `meta`: object.
- `EventsResponse` (object). Properties: `data`: array of Event; `meta`: EventsMeta.
- `EventResponse` (object). Properties: `data`: Event; `meta`: EventMeta.
- `EventsMeta` (object). Properties: `requestId`: string; `generatedAt`: string (date-time); `bookstore?`: BookstoreIdentity; `subject`: ApiSubjectIdentity; `provider`: Provider; `window`: object; `count`: integer.
- `EventMeta` (object). Properties: `requestId`: string; `generatedAt`: string (date-time); `bookstore?`: BookstoreIdentity; `subject`: ApiSubjectIdentity; `provider`: Provider; `window`: null; `count`: null.
- `BookstoreIdentity` (object). Properties: `id`: string; `name`: string; `familyName?`: string | null; `locationName?`: string | null.
- `ApiSubjectIdentity` (object). Properties: `id`: string; `name`: string; `familyName?`: string | null; `locationName?`: string | null; `type`: "bookstore" | "literaryOrganization".
- `Provider` (object). Properties: `name`: "Lit Circle"; `url`: string (uri).
- `Event` (object). Properties: `id`: string; `seriesId`: string | null; `title`: string; `description`: Description | null; `schedule`: Schedule; `eventTypes`: array of string; `genres`: array of string; `ageRating`: "allAges" | "kids" | "teens" | "adults"; `bookstoreRelationships`: array of "venue" | "primaryPresenter" | "presentedWithPartner" | "stop"; `presenters`: Presenters; `stops`: array of Stop; `venue`: Venue; `people`: array of Person; `books`: array of Book; `admission`: Admission; `image`: EventImage | null; `canonicalUrl`: string (uri); `updatedAt`: string (date-time).
- `Description` (object). Properties: `markdown`: string; `text`: string.
- `Schedule` (object). Properties: `startsAt`: string (date-time); `endsAt`: string (date-time) | null; `timeZone`: string.
- `Presenters` (object). Properties: `primary`: Presenter; `presentedWith`: array of Presenter.
- `Presenter` (object). Properties: `id`: string; `type`: "bookstore" | "literaryOrganization"; `name`: string; `familyName`: string | null; `locationName`: string | null; `canonicalUrl`: string (uri).
- `Venue` (object). Properties: `kind`: "bookstore" | "offsite" | "virtual" | "multiStop"; `name`: string; `familyName?`: string | null; `locationName?`: string | null; `description`: string | null; `address`: Address | null; `mapsUrl`: string (uri) | null; `joinUrl`: string (uri) | null.
- `Stop` (object). Properties: `id`: string; `type`: "bookstore" | "literaryOrganization"; `name`: string; `note`: string | null; `address`: Address | null; `mapsUrl`: string (uri) | null; `canonicalUrl`: string (uri).
- `Address` (object). Properties: `street`: string | null; `city`: string | null; `region`: string | null; `postalCode`: string | null; `neighborhood`: Neighborhood | null.
- `Neighborhood` (object). Properties: `id`: string; `name`: string.
- `Person` (object). Properties: `id`: string | null; `role`: "author" | "presenter"; `position`: integer; `name`: string; `bio`: string | null; `image`: PersonImage | null.
- `PersonImage` (object). Properties: `alt`: string; `originalUrl`: string (uri); `cardUrl`: string (uri).
- `Book` (object). Properties: `id`: string | null; `role`: "featured" | "additional"; `position`: integer; `title`: string; `subtitle`: string | null; `authors`: array of string; `description`: string | null; `isbn10`: string | null; `isbn13`: string | null; `publisher`: string | null; `publishedDate`: string | null; `cover`: BookCover | null; `purchaseOptions`: array of PurchaseOption.
- `BookCover` (object). Properties: `alt`: string; `originalUrl`: string (uri); `coverUrl`: string (uri).
- `PurchaseOption` (object). Properties: `provider`: "store" | "bookshop" | "libro"; `label`: string; `url`: string (uri).
- `Admission` (object). Properties: `isFree`: boolean; `priceText`: string | null; `ticketIncludesBook`: boolean; `registration`: Registration; `ticketUrl`: string (uri) | null.
- `Registration` (object). Properties: `required`: boolean; `provider`: "litCircle" | "external" | null; `url`: string (uri) | null.
- `EventImage` (object). Properties: `alt`: string; `originalUrl`: string (uri); `cardUrl`: string (uri); `listingUrl`: string (uri).
- `ErrorResponse` (object). Properties: `error`: object.