Skip to content

Content Manifest API

The Content Manifest API exposes what happened on screen during an event as a searchable, timestamped timeline. It turns the actions your operators took live — speaker lower thirds, Bible verses, songs, and info banners — into structured, machine-readable metadata you can pull into a Media Asset Management (MAM) system or a post-production workflow.

Typical uses:

  • Automated chaptering — generate video chapters from speaker changes.
  • Deep content search — make your video library searchable by the exact Bible verse or song shown during a meeting.
  • Post-production speed — hand editors the exact instant each segment began, ready to line up against the recording (e.g. "the sermon started at 10:14:05").
  • Asset enrichment — auto-tag media files with metadata extracted from the live session.

Endpoint

All API requests go to:

https://api.playout.studio

The manifest endpoint is:

GET https://api.playout.studio/manifest/:tenant/:event
ParameterInDescription
tenantpathYour tenant id.
eventpathThe event id to build the manifest for.
typequeryOptional. Comma-separated content-type filter (see Types). Case-insensitive.

Authentication

Two schemes are accepted; the tenant API key is the intended one for integrations.

Tenant API key (recommended for MAM systems and scripts) — generate it in Settings → General → API Key. The key is shown once at generation; store it in your integration's secret store. Send it in a header — either works:

X-Api-Key: <api-key>
Authorization: Bearer <api-key>

The key is scoped to your tenant: it can only read events under the tenant it belongs to. Regenerating the key invalidates the previous one immediately. API keys are never accepted in the URL or query string.

Firebase ID token (for interactive testing) — a signed-in tenant administrator's token also works:

Authorization: Bearer <firebase-id-token>

Requests are rate limited to 60 per minute, per tenant and client IP; exceeding the limit returns 429. Treat it as abuse damping rather than a precise quota — back off and retry on a 429.

Example

bash
curl -H "X-Api-Key: $API_KEY" \
  "https://api.playout.studio/manifest/my-church/sunday-service-2026-06-24?type=SPEAKER,SCRIPTURE"

Response

json
{
  "eventId": "sunday-service-2026-06-24",
  "eventStart": "2026-06-24T10:00:00.000Z",
  "eventStartSource": "scheduled",
  "manifest": [
    {
      "type": "SPEAKER",
      "label": "John Doe",
      "timestamp": "2026-06-24T10:05:42.000Z",
      "data": {
        "personId": 11629,
        "uid": "9c8f3b2a-1d4e-4f7a-b6c2-0e5a7d1f8b34",
        "name": "John Doe"
      }
    },
    {
      "type": "SCRIPTURE",
      "label": "Psalm 23:1",
      "timestamp": "2026-06-24T10:20:05.000Z",
      "data": {
        "book": "psalms",
        "chapter": 23,
        "verseFrom": 1,
        "verseTo": 1,
        "label": "Psalm 23:1"
      }
    }
  ]
}

Envelope fields

FieldTypeDescription
eventIdstringThe event id.
eventStartstring | nullISO 8601 instant the event began.
eventStartSourcestring | nullHow the start was anchored: scheduled (from the event's date + start time, in UTC) or firstAction (the first content action, used when no scheduled start is set).
manifestarrayThe content events, oldest first.

Entry fields

FieldTypeDescription
typestringOne of the content types.
labelstringA human-readable label, always present.
timestampstringExact time of the action, ISO 8601 (UTC). Authoritative — derive any offset you need from this.
dataobjectType-specific structured metadata (see below).

Only content-bearing actions appear. Show/hide toggles, blackouts, and timer actions are operational state changes, not content, and are omitted.

offsetSec has been removed

Entries used to carry an offsetSec — seconds from eventStart. It has been removed: it was measured from the event's scheduled start, which rarely matches when anything actually happened, and it was clamped at zero, so every action occurring before the scheduled time collapsed to 0 and lost its ordering.

Use timestamp instead. It is exact, absolute, and unambiguous. To get an offset against your own reference point — typically the moment your recording started — subtract the two instants:

js
const offsetSec = (Date.parse(entry.timestamp) - Date.parse(recordingStart)) / 1000

This gives an offset that lines up with your video file, which the old field did not.

Legacy events

Events recorded before structured metadata was captured still export: their entries carry the correct type, label and timestamp, with a data object containing at least { "label": "…" }.

The oldest events — recorded before the audit trail used structured action codes at all — are brought forward by a one-time database migration, which also reconstructs the structured data on a best-effort basis: speakers are matched by name, songs by catalogue number or title, and scripture references are parsed from the verse label.

Reconstruction is conservative. Where the original record could not be identified with confidence — a person no longer in the database, or two people sharing a display name — the individual fields are left null rather than guessed, while label, type and timestamp remain correct. Treat identifiers on these older entries as best-effort, and the label as authoritative.

Content types

Filter with ?type= using any combination of the following.

SPEAKER — person lower third

json
{ "personId": 11629, "uid": "9c8f3b2a-1d4e-4f7a-b6c2-0e5a7d1f8b34", "name": "John Doe" }
FieldDescription
personIdThe numeric person id.
uidThe person's stable uid, as used by the core API. null if unknown.
nameDisplay name at the time the lower third was shown.

personId and uid are two different identifiers for the same person — match on whichever your system stores.

uid on older events

uid was previously populated with an internal record id that happened to equal personId. Those entries have been corrected by a one-time migration, and events recorded since carry the real core API uid.

A small number could not be corrected — where the person has since left the catalogue, or has no uid recorded upstream. Those keep the old value, so uid still equals personId. If you see that, treat uid as unavailable for that entry and match on personId.

SONG — song lower third

json
{
  "uid": "song-abc",
  "key": "HV 123",
  "metadata": {
    "author": "John Newton",
    "composer": null,
    "arranger": null,
    "soloist": null
  }
}

SCRIPTURE — Bible verse

json
{ "book": "psalms", "chapter": 23, "verseFrom": 1, "verseTo": 1, "label": "Psalm 23:1" }
FieldTypeDescription
bookstring | nullStable book identifier (psalms, 1john) — see below.
chapternumber | nullChapter number.
verseFromnumber | nullFirst verse of the range.
verseTonumber | nullLast verse; equals verseFrom for a single verse.
labelstring | nullHuman-readable reference, e.g. Psalm 23:1.

book is an identifier, not a display name — it is consistent across translations, so it is safe to match on. It stays a string because there is no meaningful number for a book; chapter and the verse fields are always numbers. Use label for the human-readable reference.

Any of these may be null on older events where the value could not be recovered — label is the field that is always meaningful.

TWOLINES — info / two-line text lower third

json
{ "text": "Welcome to the service" }

SLIDE

Reserved for screen/slide changes. Accepted by the type filter but not emitted yet, as screen changes are not currently recorded in the event's content trail.

Errors

StatusMeaning
400An unknown value was passed to type, or the tenant does not exist.
401Missing/invalid API key or token, or not a tenant administrator.
404The event does not exist for that tenant.
429Rate limit exceeded (60 requests/minute per tenant and client IP).
500The manifest could not be built.

All rights reserved.