Appearance
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.studioThe manifest endpoint is:
GET https://api.playout.studio/manifest/:tenant/:event| Parameter | In | Description |
|---|---|---|
tenant | path | Your tenant id. |
event | path | The event id to build the manifest for. |
type | query | Optional. 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
| Field | Type | Description |
|---|---|---|
eventId | string | The event id. |
eventStart | string | null | ISO 8601 instant the event began. |
eventStartSource | string | null | How 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). |
manifest | array | The content events, oldest first. |
Entry fields
| Field | Type | Description |
|---|---|---|
type | string | One of the content types. |
label | string | A human-readable label, always present. |
timestamp | string | Exact time of the action, ISO 8601 (UTC). Authoritative — derive any offset you need from this. |
data | object | Type-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)) / 1000This 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" }| Field | Description |
|---|---|
personId | The numeric person id. |
uid | The person's stable uid, as used by the core API. null if unknown. |
name | Display 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" }| Field | Type | Description |
|---|---|---|
book | string | null | Stable book identifier (psalms, 1john) — see below. |
chapter | number | null | Chapter number. |
verseFrom | number | null | First verse of the range. |
verseTo | number | null | Last verse; equals verseFrom for a single verse. |
label | string | null | Human-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
| Status | Meaning |
|---|---|
400 | An unknown value was passed to type, or the tenant does not exist. |
401 | Missing/invalid API key or token, or not a tenant administrator. |
404 | The event does not exist for that tenant. |
429 | Rate limit exceeded (60 requests/minute per tenant and client IP). |
500 | The manifest could not be built. |