Skip to content

Events

FrameWorks records a public event whenever something in your account changes state: a stream goes live, a clip becomes ready, an upload fails processing, an invoice is issued. The service that owns the change writes the event in the same transaction as the change itself, so an event is never sent for a change that did not commit.

Two ways deliver them, with the same payloads:

  • the tenantEvents subscription, while your client is connected;
  • webhooks: signed HTTPS requests to your endpoints, retried for about three days, with a 30-day delivery log and replay.

The SDKs wrap this subscription: they reconnect with backoff and read the current token again on every reconnect. Over raw GraphQL:

subscription ClipEvents($streamId: ID) {
tenantEvents(types: ["clip.ready", "clip.failed"], streamId: $streamId) {
id
type
time
subject
data {
__typename
... on ClipReady {
artifact {
artifactId
streamId
}
durationMs
sizeBytes
}
... on ClipFailed {
artifact {
artifactId
}
reason
}
}
}
}

Both arguments are optional:

  • types passes only the listed event types, compared as exact strings ("clip.ready", not "clip.*"). Omit it to receive every type your permissions allow.
  • streamId passes only events whose payload names that stream: streamId on stream and multistream events, artifact.streamId on clip and recording events. Uploads, API tokens, billing, account, and custom domain events name no stream, so they never match a streamId filter.

Subscriptions run over the WebSocket endpoint described in the API reference. You receive only your own account’s events.

Stream, clip, recording, upload, and multistream events require streams:read; billing and account events require billing:read; API token and custom domain events require developer:read. Explicitly requesting a type you cannot read returns FORBIDDEN. An omitted types filter is narrowed to your permissions; it does not grant access to other categories.

FieldMeaning
idEvent ID. The same event keeps the same ID wherever it is delivered; use it to deduplicate.
typeThe event type from the catalog below, e.g. stream.live.
timeWhen the change committed.
subjectThe object the event is about, as <aggregate>/<id>, e.g. streams/<stream id>.
dataThe payload of the type. In GraphQL it is a union; select fields with ... on <PayloadType>.

The field names in the tables below are the JSON names of each payload, exactly as GraphQL returns them and as webhook bodies carry them. Within this version, fields are only added, never renamed or removed; a breaking change ships as a new version of the payload. Enum values are the full upper-case names shown here, such as MEDIA_FAILURE_REASON_PROCESSING_FAILED. Amounts are { amountMinor, currency }: an integer in the currency’s minor unit (cents for EUR) and an ISO 4217 code.

Payloads carry only identifiers and values your account already sees through the API. They never contain internal stream names, node IDs, storage paths, URLs, IP addresses, or raw error text. Media failures carry a coarse reason; the error detail for an artifact is on the artifact itself.

Clip, recording, and upload events share one artifact object:

FieldMeaning
artifactIdThe artifact’s public hash, the hash of its storageArtifactsConnection row.
kindARTIFACT_KIND_CLIP, ARTIFACT_KIND_RECORDING, or ARTIFACT_KIND_UPLOAD.
streamIdThe source stream; empty for uploads.
playbackIdEmpty, except on recording.ready (see below).

Events do not carry the artifact’s playback ID. A playback ID can be rotated, so key your records on artifactId and look the current playback ID up when you need it: query storageArtifactsConnection with artifactHash set to the artifactId, or clip(id:) for a clip.

Media failure reasons: MEDIA_FAILURE_REASON_SOURCE_UNAVAILABLE, _PROCESSING_FAILED, _STORAGE_FAILED, _INVALID_INPUT, _QUOTA_EXCEEDED, _TIMED_OUT (each prefixed MEDIA_FAILURE_REASON).

Subject: streams/<stream id>.

TypeFires whenPayload
stream.createdA stream is created.streamId, name, playbackId
stream.updatedStream settings, stream keys, or multistream targets change.streamId, changedFields (snake_case field identifiers)
stream.deletedA stream is deleted.streamId
stream.key_rotatedThe stream key is replaced. The key itself is never in the event.streamId
stream.connectedAn encoder or pull source starts a new ingest session.streamId, playbackId, protocol (INGEST_PROTOCOL_RTMP, _SRT, _WHIP, _PULL)
stream.liveThe stream becomes playable.streamId, playbackId
stream.idleThe ingest session ends.streamId, playbackId

changedFields values are snake_case identifiers, not GraphQL field names: title, description, is_recording_enabled, live_video_abr, pull_source, source_location, dvr_chapter_mode, dvr_chapter_interval_seconds, monitoring_enabled, playback_policy, stream_keys, and push_targets.

stream.connected, stream.live, and stream.idle fire once per ingest session. Their playbackId is the stream’s playback ID when the event was emitted; on stream.live and stream.idle it is empty in the rare case the platform could not resolve the stream while its control plane was unreachable. A playback ID can be rotated, so key your records on streamId.

Subject: artifacts/<artifact id>.

TypeFires whenPayload
clip.requestedA clip is requested.artifact, durationMs (requested length)
clip.readyThe clip is stored and playable.artifact, durationMs, sizeBytes
clip.failedThe clip could not be produced.artifact, reason
recording.startedThe recording node confirmed it is capturing the stream.artifact
recording.stoppedCapture ended and the recording is being finalized.artifact
recording.readyThe recording’s first saved chapter is replayable.artifact, durationMs, sizeBytes
recording.failedA DVR recording could not be finalized.artifact, reason
upload.createdA VOD upload session is created.artifact, filename, expectedSizeBytes
upload.completedEvery part is uploaded and the file is handed to processing.artifact, sizeBytes
upload.abortedThe upload is aborted before completion.artifact
upload.readyProcessing finished and the asset is playable.artifact, durationMs, sizeBytes
upload.failedProcessing rejected or could not process the file.artifact, reason

A recording that captured media emits recording.started, then recording.stopped once. recording.ready fires once, when the recording’s first saved chapter becomes replayable: its artifact.playbackId plays that chapter, and durationMs and sizeBytes describe it. A short broadcast gets it after recording.stopped; a broadcast longer than one chapter gets it while still live. recording.failed reports a recording that could not be finalized. A live-rewind-only recording (stream dvrChapterMode: NONE) keeps nothing replayable, so it ends with recording.stopped and no recording.ready. A recording that never captured anything emits only recording.failed.

Upload events carry no transfer progress. Your client knows its own transfer progress; for the server-side state of an upload, query vodUploadStatus. An import from a URL emits the same upload events. upload.created fires when the import is accepted, with expectedSizeBytes "0". upload.completed fires once the processing node has fetched the source and recognized it as media, and its sizeBytes is the source file’s size. An import whose source cannot be fetched (for example a 404) emits upload.failed and no upload.completed.

Subject: push_targets/<target id>.

TypeFires whenPayload
multistream.status_changedA multistream target’s push status changes.streamId, targetId, targetName, status, previousStatus

status and previousStatus are MULTISTREAM_STATUS_IDLE, _PENDING, _PUSHING, _RETRYING, _STOPPING, or _FAILED (each prefixed MULTISTREAM_STATUS). targetName is your label for the target, never its URL. The public target’s reasonCode can be media_selection_failed when Mist cannot find a compatible video and audio pair; webhook payloads do not include destination URIs or raw Mist logs.

Subject: api_tokens/<token id>.

TypeFires whenPayload
api_token.createdAn API token is created.tokenId, name, permissions, expiresAt (null if none)
api_token.revokedAn API token is revoked.tokenId
TypeSubjectFires whenPayload
billing.invoice_createdinvoices/<invoice id>An invoice is issued. Drafts and invoices held for review emit nothing.invoiceId, amountDue, periodStart, periodEnd, dueAt
billing.invoice_paidinvoices/<invoice id>An invoice is paid.invoiceId, amountPaid
billing.payment_failedpayments/<payment id>A charge fails.paymentId, invoiceId, amount, reason, provider, providerReferenceId
billing.topup_creditedtopups/<top-up id>A prepaid top-up is credited to your balance.topupId, amount
billing.details_updatedtenants/<account id>Your billing details change.changedFields

A charge your payment provider starts on its own subscription leaves paymentId and invoiceId empty and names the provider (provider, e.g. stripe) and its identifier for the charge (providerReferenceId). Payment failure reasons are PAYMENT_FAILURE_REASON_DECLINED, _INSUFFICIENT_FUNDS, _EXPIRED_METHOD, _AUTHENTICATION_REQUIRED, and _PROCESSING_ERROR.

TypeSubjectFires whenPayload
account.suspendedtenants/<account id>Your account is suspended.reason
custom_domain.verifiedcustom_domains/<domain>A custom domain passes verification.domain
custom_domain.failedcustom_domains/<domain>A custom domain fails verification or TLS.domain, reason

Suspension reasons: SUSPENSION_REASON_BALANCE_EXHAUSTED, _PAYMENT_OVERDUE, _POLICY. Custom domain failure reasons: CUSTOM_DOMAIN_FAILURE_REASON_VERIFICATION_EXPIRED (verification did not succeed within the verification period) and _CERTIFICATE_FAILED.

In GraphQL, the reason fields of payment, account, and custom domain events have different enum types from the media reason. When one selection covers several of them, alias the field, for example suspensionReason: reason.