LiveCal
Publisher sections ›
Documentation

Publisher API reference

The core, live in production at livecal.ai/api/v1. Create Sources, push live-state, and read aggregate analytics. New here? Start with the quickstart, then grab a key from the dashboard.

Live. The Source API, the MCP server, the Track Widget, and the WordPress plugin are shipped and callable in production. Push-source facts render as a structured, labeled block with the (tentative) marker; the publisher-declared typed facts_schema (pull lane) is still deferred.

Authentication

Exchange your site key for a short-lived JWT, then send it as a bearer token. $LIVECAL_TOKEN below is a placeholder — request a real per-site key out-of-band and never inline it.

POST /api/v1/auth/token
curl -X POST https://livecal.ai/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{ "grant_type": "site_key", "api_key": "livecal_sk_…" }'
// 200 OK -> { "token": "<jwt>", "expires_in": ... }
// Site keys are app-scoped — NO user_email. Send the JWT as
//   Authorization: Bearer <jwt>
// on every Source call below. (user_email is the legacy user_key grant only.)
Endpoint
PUT /api/v1/sources/{external_id}Upsert a Source. Idempotent on your external_id — safe to replay.
POST /api/v1/sources/batchUpsert up to 100 Sources in one call. Partial-success: each row reports its own result.
POST /api/v1/sources/{id}/statePush a live-state update. Debounced — only material changes reach trackers' calendars.
POST /api/v1/sources/validateDry-run a Source — validates payload shape + time fields. No writes, no trackers touched.
GET /api/v1/sources/{id}Read a Source's current state, facts, and feed flags.

Create a Source — idempotent on your external_id

Each fact carries a confidence of "known" or "provisional". starts_at is required (ISO 8601); ends_at + all_day cover multi-day and all-day events (for all-day, ends_at is the inclusive last day).

PUT /api/v1/sources/{external_id}
curl -X PUT https://livecal.ai/api/v1/sources/oshkosh-airshow-2026 \
  -H "Authorization: Bearer $LIVECAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "manifest": "airshow",
    "title": "EAA AirVenture Oshkosh — Daily Air Show",
    "starts_at": "2026-07-20T13:00:00-05:00",
    "subscribe_url": "https://example.com/oshkosh-2026",
    "facts": {
      "venue":        { "value": "Wittman Regional Airport", "confidence": "known" },
      "headline_act": { "value": "USAF Thunderbirds",        "confidence": "provisional" },
      "gates_open":   { "value": "07:00",                    "confidence": "known" }
    }
  }'

// Multi-day + all-day events: set all_day:true with bare YYYY-MM-DD dates, and
// ends_at as the INCLUSIVE last day. This writes a real two-day all-day block
// (Aug 1 + Aug 2) in every subscriber's calendar:
//   { "manifest":"airshow", "title":"Seafair Air Show",
//     "all_day":true, "starts_at":"2026-08-01", "ends_at":"2026-08-02" }
Response
// 201 Created  (200 OK on update — idempotent on your external_id)
// source_id is a UUID LiveCal mints; use it for the widget + the /state path.
{
  "source_id": "3f9a21c8-7b4e-4d2a-9c1f-0a2b3c4d5e6f",
  "widget_id": "3f9a21c8-7b4e-4d2a-9c1f-0a2b3c4d5e6f",
  "widget_url": "https://livecal.ai/s/3f9a21c8-7b4e-4d2a-9c1f-0a2b3c4d5e6f"
}

Push a live-state update

Send the new live-state line and any changed facts. LiveCal debounces — only material changes reach a tracker's calendar.

POST /api/v1/sources/{id}/state
curl -X POST https://livecal.ai/api/v1/sources/3f9a21c8-…/state \
  -H "Authorization: Bearer $LIVECAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "live",
    "live_state": "Gates open — Thunderbirds wheels-up 15:30",
    "facts": { "headline_act": { "value": "USAF Thunderbirds", "confidence": "known" } }
  }'
// 200 OK -> { source_id, propagated: bool, reason, field?, fan_out? }
// LiveCal debounces: state is always stored, but a subscriber-facing PATCH
// fires only on a material change (propagated=true).

Manifests & confidence tiers

A Manifest slug (airshow, flights, earnings, …) selects the facts vocabulary for a Source. Promoting a fact from provisional to known is a material change that updates trackers; because LiveCal debounces, you can write freely as information firms up.

i

Slug handling. The manifest slug is stored as-sent; it is not yet validated against a catalog — a typo gets a clean 201 and falls back to a generic render. Push-source facts render as a structured, labeled block today; the publisher-declared typed facts_schema in your manifest (pull lane) is still deferred.

Manifest — airshow (illustrative)
{
  "manifest": "airshow",
  "facts": {
    "venue":        { "type": "string", "required": true  },
    "gates_open":   { "type": "time",   "required": false },
    "headline_act": { "type": "string", "required": false },
    "status":       { "type": "enum", "values": ["scheduled","delayed","flying","done"] }
  }
}
// Every fact carries confidence: "known" | "provisional".
// A provisional -> known transition is a material change and updates trackers.

The error contract

Every error returns the same envelope. The retryable flag tells you whether to back off and retry (429 / 5xx, honoring Retry-After) or to fix the request and resend (4xx / 422).

Error envelope
{
  "error": { "code": "rate.limited", "message": "Too many state pushes.", "retryable": true }
}
// "field" is present only on field-level (422) errors — e.g. "facts.gates_open".
// Retryable (back off + retry):   429, 500, 502, 503, 504 — honor Retry-After.
// Terminal  (fix and resend):     400, 401, 403, 404, 409, 422.

Analytics you get

Aggregate and privacy-safe, over two app-scoped REST endpoints — GET /api/v1/apps/{id}/stats and GET /api/v1/apps/{id}/funnel. No PII — never who tracks you, only how many. Any bucket below five trackers is suppressed (k-anonymity).

MetricWhat it covers
Widget funnel & conversionImpressions → Track clicks → completed Tracks, by state (Unauthenticated / Recognized / Tracking).
Tracker count, growth & churnNet trackers over time per Source, plus stop-tracking rate.
Provider splitShare of trackers on Google vs. Apple calendars.
Propagation latencyTime from your state push to the PATCH landing on tracker calendars (p50 / p95).
Tracking-options engagementWhich per-App update toggles trackers keep on vs. mute.

Ingestion — push-preferred

If you push your updates, we never poll you — your state pushes are the source of truth and propagate the instant they land. We poll only third-party feeds that nobody pushes, and retire that polling as push paths appear. The destination is a publisher-owned push for every Source.

Integration libraries (the Widget embed, the WordPress plugin, the MCP server) live on Libraries →