# MediaKind Docs > Developer documentation for MediaKind products, APIs, and workflows. Markdown versions are available for each linked documentation page. # API Guides Explore the MediaKind API guides to learn how to integrate MediaKind's services into your workflows. ## Getting Started Prepare your environment, authenticate your API calls, and learn how the platform represents resources. - [Quick Start & Authentication](/api-guides/getting-started/authentication): Get your API credentials, authenticate your requests, and make your first API call. - [Shared Concepts](/api-guides/understanding/overview): Understand platform-wide concepts like pagination, rate limiting, error handling, and resource states. ## Media API Ingest, transcode, package, protect, and publish live and on-demand video assets. - [Media API Reference](/api-reference/media-api): Interactive endpoints, schemas, and request/response examples for the Media API. - [Automate a VOD Pipeline](/api-guides/how-to/media/vod-pipeline): A complete recipe from upload through encoding and packaging to playout. - [Storage](/api-guides/how-to/media/storage): Configure input and output storage containers. - [Assets](/api-guides/how-to/media/assets): Upload media files and track asset lifecycles. - [Transforms & Jobs](/api-guides/how-to/media/transforms-and-jobs): Define transcode settings and submit transcode jobs. - [Live Streaming](/api-guides/how-to/media/live-streaming): Configure live events, inputs, outputs, and streaming channels. - [Publishing & Delivery](/api-guides/how-to/media/publishing): Create streaming locators and construct playback URLs. - [Content Protection](/api-guides/how-to/media/content-protection): Apply DRM and content key policies to secure streams. ## Management API Control organizations, users, teams, projects, webhook notifications, and usage reporting. - [Management API Reference](/api-reference/management-api): Endpoints, schemas, and request/response examples for organization management. - [Provision Organizations & Users](/api-guides/how-to/management/org-provisioning): Bootstrap organizations, structure projects, and define team memberships. - [Tokens](/api-guides/how-to/management/tokens): Create and rotate personal access tokens. - [Webhook Rules](/api-guides/how-to/management/webhooks): Configure HTTP callbacks to receive event-driven notifications. - [Usage & Billing](/api-guides/how-to/management/usage-and-billing): Query billing telemetry and track resource consumption. ## Fleets API Register, update, and manage hardware and software device inventory for on-premises edge deployments. - [Fleets API Reference](/api-reference/fleets-api): Endpoints, schemas, and request/response examples for fleet operations. - [Manage Fleet Devices](/api-guides/how-to/fleets/device-management): Register devices, configure settings, and monitor edge appliance health. - [Backups & Restores](/api-guides/how-to/fleets/backups-and-restores): Backup device configurations and restore edge states. - [Software & Updates](/api-guides/how-to/fleets/software-and-updates): Manage device software versions and roll out fleet-wide updates. ## Infrastructure APIs Configure network settings and sites across the platform. - [Infrastructure API Reference](/api-reference/infrastructure-api): Endpoints, schemas, and details for infrastructure resource operations. - [Set up Networks & Sites](/api-guides/how-to/infrastructure/networks-and-sites): Provision network configuration and link sites. # Get started with the APIs Use these guides to choose the right API, create a personal token, and confirm your access with a first request. - [Which API to use](/api-guides/getting-started/which-api): Match your workflow to the Media, Management, Fleets, or Infrastructure API. - [Authentication and tokens](/api-guides/getting-started/authentication): Create a personal API token and send it with your requests. - [Your first API call](/api-guides/getting-started/first-api-call): Make one authenticated request to confirm your token and project access. # Authentication and tokens The MK.IO APIs use bearer authentication. You create a personal API token in the MK.IO web application, send it in the `Authorization` header, and the request runs with the same organization and project access as the user who created it. ## Create a personal token Create a token in the MK.IO web application: 1. Open the profile menu in the top right corner and select your email address. 2. Go to [**Your personal API tokens**](https://app.mk.io/user/profile). 3. Select **Add Token**. 4. Enter a description and an expiration date in UTC. 5. Select **Create**. 6. Copy the token to a secure location. It is visible for 5 minutes only. For the full token-management pages in the MK.IO product, see [API tokens](/mkio/how-to/managing-your-organization/api-tokens). ## What a personal token represents The personal token contains: - The identity of the user. - The identity of the organization. - The token type and expiration date. - Optionally, a reduced set of permissions when the token grants only a subset of the user's access. The token therefore carries the access of the user who created it. When that user's access changes, the token's effective access changes with it. ## Send the token with every request Every MK.IO API uses the same header: ```http Authorization: Bearer ``` For requests that carry a body, send JSON headers as well: ```http Content-Type: application/json Accept: application/json ``` A complete request looks like this: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` Replace `` with the project you want to query and `` with the token you created. ## Base URL The MK.IO APIs share one base URL: ``` https://app.mk.io ``` Combine it with the path for the endpoint you want to call, for example: ``` https://app.mk.io/api/v1/projects//media/assets ``` ## A practical local setup When you work from the command line, export the values you reuse: ```bash ``` Then reference them in later commands: ```bash curl -X GET "$MKIO_BASE_URL/api/v1/projects/$MKIO_PROJECT/media/assets" \ -H "Authorization: Bearer $MKIO_TOKEN" \ -H "Accept: application/json" ``` This keeps the examples short and makes it easier to switch projects or rotate tokens. ## Keep tokens out of source code A token grants MK.IO access to anyone who holds it, so treat it as a secret: - Do not commit tokens to source control. - Do not embed them in client-side code or public applications. - Rotate and replace them if they are exposed. - Use a narrower token type when you want to reduce the blast radius. See [Tokens](/api-guides/how-to/management/tokens) for the Management API workflow. ## Next steps - [Your first API call](/api-guides/getting-started/first-api-call): confirm that your token and project access work. - [Tokens](/api-guides/how-to/management/tokens): create, inspect, and revoke tokens through the Management API. - [Which API to use](/api-guides/getting-started/which-api): choose the API that owns the resources in your workflow. # Your first API call Before you build a larger integration, confirm your setup with one request. The goal is to make a single authenticated call against a project-scoped endpoint, inspect the response, and prove that your token and project access work. ## Prerequisites You need: - An MK.IO account with access to at least one project. - A personal API token. - The name of the project you want to query. If you still need a token, follow [Authentication and tokens](/api-guides/getting-started/authentication). ## Set the values you reuse Export the values the request needs, so the command stays readable and retries are less error-prone: ```bash ``` ## Send the request Run the Media API list-assets operation: ```bash curl -X GET "$MKIO_BASE_URL/api/v1/projects/$MKIO_PROJECT/media/assets" \ -H "Authorization: Bearer $MKIO_TOKEN" \ -H "Accept: application/json" ``` To see the HTTP status line as well, add `-i`: ```bash curl -i -X GET "$MKIO_BASE_URL/api/v1/projects/$MKIO_PROJECT/media/assets" \ -H "Authorization: Bearer $MKIO_TOKEN" \ -H "Accept: application/json" ``` ## Check the response A successful response is HTTP `200` with a list body. List endpoints return two top-level fields: - `value`: the returned assets. - `supplemental`: list metadata, including `supplemental.pagination` with the record counts. ```json { "value": [ { "name": "example-asset" } ], "supplemental": { "count": 1, "kind": "Asset", "operation": "list", "pagination": { "start": 0, "end": 1, "records": 1, "total": 1 } } } ``` Many MK.IO list endpoints use this same shape, so it is worth recognising now. See [Pagination and filtering](/api-guides/understanding/pagination) for how to page and narrow these responses. ## If the call does not succeed This endpoint returns the platform's standard error responses: | Status code | Meaning | What to check next | | :--- | :--- | :--- | | `400` | Bad Request | Recheck the URL, query parameters, and request syntax. | | `401` | Unauthorized | Recheck the bearer token and `Authorization` header. | | `403` | Forbidden | Recheck that the user has access to the target project and operation. | | `404` | Not Found | Recheck the project name and path. | | `429` | Too Many Requests | Slow down and retry later. | | `500` | Internal Server Error | Record the response `ref` value before retrying or contacting support. | The error body carries `error.code` for programmatic handling, `error.detail` for logs, and `ref` for support follow-up. See [Error handling](/api-guides/understanding/error-handling) for the full model. ## What this confirms A successful request proves that your token is valid, your base URL is correct, your project name is correct, and you can reach a real project-scoped endpoint. That is enough to move on to resource creation with far less guesswork. ## Next steps - [Which API to use](/api-guides/getting-started/which-api): choose the API that owns your workflow. - [API overview](/api-guides/understanding/overview): the patterns shared across the APIs. - [Build with the Media API](/api-guides/how-to/media): move into storage, assets, transforms, jobs, and publishing. # Which API to use Choose an MK.IO API based on the resource you need to manage, not the kind of code you are writing. The APIs share a base URL and bearer authentication, but they own different parts of the platform and are often combined in a single automation flow. ## Start with the workflow Use this table for a quick answer: | To automate... | Use the... | | :--- | :--- | | Storage registration, assets, transforms, jobs, live events, streaming locators, streaming endpoints, streaming policies, or content key policies | [Media API](/api-reference/media-api) | | Organizations, projects, users, teams, tokens, payment methods, usage reports, metrics, or webhook rules | [Management API](/api-reference/management-api) | | Beam device registration, backups, restores, software rollouts, or support packages | [Fleets API](/api-reference/fleets-api) | | Networks and sites for Beam deployments | [Infrastructure API](/api-reference/infrastructure-api) | ## How the APIs fit together The APIs are separate, but the workflows connect: 1. Use the **Management API** to create a project and assign billing. 2. Use the **Media API** to process or publish content inside that project. 3. Use the **Management API** again to add webhook rules, so your integration reacts to events instead of polling. 4. Use the **Infrastructure API** to define sites and networks for a Beam deployment. 5. Use the **Fleets API** to onboard and operate the Beam devices in those sites. A complete platform integration typically uses the Management API for setup, then the Media, Fleets, or Infrastructure API for day-to-day operations. ## Media API The Media API owns media processing and delivery, and it is where most developers start. Reach for it to ingest, encode, package, protect, or stream content. Its resources include storage instances and credentials, assets and asset filters, transforms and jobs, live events and live outputs, streaming locators and endpoints, and streaming policies and content key policies. ## Management API The Management API owns organization and project administration: access, billing, and account-level configuration. Reach for it to provision an environment, assign permissions, issue tokens, or configure event delivery. Its resources include organizations and projects, users and invites, teams, roles and scopes, personal and organization tokens, payment methods, usage and metrics, and webhook rules. ## Fleets API The Fleets API is the project-scoped operational API for MK.IO Beam, the on-premises product. Reach for it to register devices, protect device state with backups, restore configurations, manage software versions, and collect support packages. Its resources include device inventory and registration, backups and restores, software selection and preload behaviour, and support packages and diagnostics. ## Infrastructure API The Infrastructure API defines where Beam devices live and which networks are available to them, so it is usually used before or alongside the Fleets API. Its resources include networks, sites, routes from sites to networks, and label-driven filtering of network and site resources. ## Common combinations | Workflow | APIs involved | | :--- | :--- | | Create a project, assign billing, then start a VOD workflow | Management API, then Media API | | Register a webhook for job-completion events | Management API and Media API | | Define sites and networks, then register Beam devices to them | Infrastructure API, then Fleets API | | Audit token usage and restrict automation access | Management API | ## Next step Once you know which API owns your workflow, make one authenticated request before you build anything larger. [Your first API call](/api-guides/getting-started/first-api-call) is the quickest way to confirm that your base URL, token, and project access are correct. # API workflow guides Choose a workflow area to find task-focused guides for building with the MK.IO APIs. - [Media API](/api-guides/how-to/media): Build VOD, live, publishing, storage, and content-protection workflows. - [Management API](/api-guides/how-to/management): Provision projects, manage access, issue tokens, configure webhooks, and report usage. - [Fleets API](/api-guides/how-to/fleets): Register Beam devices, manage software, protect configurations, and collect diagnostics. - [Infrastructure API](/api-guides/how-to/infrastructure): Create networks and sites for Beam device operations. # Build with the Channels API The Channels API is the local API of an MK.IO Beam device. It presents everything the device is doing as a number of *channels*, where one channel can cover the input, the processing, and the output of a single service. It is the API behind the Beam Essentials UI, and it runs on the device itself rather than in the MK.IO cloud. > **warning:** The Channels API requires an MK.IO Beam device running version `1.12.0` or higher. It is delivered as part of the Beam Essentials UI release, so devices on earlier versions do not serve these endpoints. ## API overview | Detail | Value | | :--- | :--- | | Base path | `/ui/connectors/channels-api/latest` | | Scope | A single Beam device | | Authentication | None beyond network access to the device | | Reference | [Channels API reference](/api-reference/channels-api) | The base URL is the device itself, not `api.mk.io`. Every other MK.IO API is project-scoped and bearer authenticated, so if you already integrate with the Media or Fleets API, treat this as a separate integration. See [Connect to a Beam device](/api-guides/how-to/channels/connect-to-a-device) for how to build the address and connect to your Beam device. ## How the main resources connect The API exposes a handful of resource types, and channels are the centre of all of them: 1. A **channel** holds a `spec` (the configuration you send) and a `status` (what the device reports back). 2. **Interfaces** are the physical and network ports available on the device. You read them to fill in the `interface` and `url` fields of a channel. 3. **Channel alarms** are the active faults raised against a named channel. 4. **Metrics** and **system** information describe the device rather than any one channel. A typical integration reads the interfaces once, creates channels against them, then polls channel status and alarms to drive a dashboard or an alerting rule. ## Common workflow patterns ## Guides - [How Beam channels work](/api-guides/how-to/channels/how-channels-work): Channel types, the split between spec and status, and how configuration is applied asynchronously. - [Connect to a Beam device](/api-guides/how-to/channels/connect-to-a-device): Build the base URL, use the Swagger page served by the device, and understand the current authentication model. - [Create and manage channels](/api-guides/how-to/channels/manage-channels): A complete worked request, the input and output types each channel type accepts, and how to start, rename, and delete. - [Monitor channels and devices](/api-guides/how-to/channels/monitor-channels): Filter the channel list, read health and alarms, fetch thumbnails, and query device metrics. - [Import and export channels](/api-guides/how-to/channels/import-and-export): Move a channel configuration between devices, or restore one after a rebuild. ## Where to go deeper - [Beam Essentials UI](/beam/essentials) covers the same channels through the device interface, including the creation wizard. - [Channels API reference](/api-reference/channels-api) is the complete field-level truth for every schema referenced in these guides. # Connect to a Beam device The Channels API is served by the Beam device, so there is no central endpoint and no token exchange. You address the device directly, on the same host and port you already use to reach its web interface. ## Build the base URL Take the address you use for the device web interface and append the API path: ```text /ui/connectors/channels-api/latest ``` `` is the scheme, host, and port of the device, for example `http://10.0.0.10:8080`. Whether you need a port, and which one, depends on how the device is deployed and secured. [Connect to the Controller](/beam/web-interface/connection) lists the access URL forms, including the ports used for containerized solutions and for HTTPS. A full request URL therefore looks like this: ```bash curl http://10.0.0.10:8080/ui/connectors/channels-api/latest/channels/ ``` ## Try it on the device first The device serves an interactive Swagger page for its own build of the API. Open it in a browser at: ```text /ui/connectors/channels-api/latest/docs/ ``` This is the fastest way to see the endpoints, and you can execute requests against the device from the page itself. Use it to confirm connectivity before you write any integration code. The [Channels API reference](/api-reference/channels-api) documents the same API on this site, so use whichever suits you. ## Authentication There is currently no authentication on the Channels API beyond network access to the device. Any client that can reach the device address can read and change its channels. > **warning:** Treat network access as the only control you have. Keep Beam devices on a management network that is not reachable from untrusted clients, and do not expose the device address publicly. ## Confirm the device version The API is only present on Beam `1.12.0` and higher. Requests to a device on an earlier version do not reach the API at all, so a connection error or a 404 on the base path is the expected symptom rather than an authentication failure. Check the version the device reports: ```bash curl /ui/connectors/channels-api/latest/system/ ``` The response wraps the device identity in a `data` object: ```json { "data": { "softwareVersion": "1.12.0.7", "softwareName": "beam", "serialNumber": "", "hardwareModel": "VEGA-7010" } } ``` `softwareVersion` carries a build number as a fourth component, so compare it part by part rather than against the string `1.12.0`. A device that does not report a value returns an empty string, as `serialNumber` does above, so check for emptiness rather than for `null`. A successful response here also confirms your base URL is correct, which makes it a useful first call. ## Check that the device can be managed A device may be on a supported version but still be running channels that predate the Channels API. `GET /deployment/eligibility/` reports whether the API can take over the device: ```bash curl /ui/connectors/channels-api/latest/deployment/eligibility/ ``` `canRun` is the verdict. `deploymentState` describes the current state of the device, and `resources` lists the existing services, feeds, and saved configurations found on it. Each entry carries `canUpgrade` and a `reason`, so when `canRun` is `false` the `resources` list tells you which object is blocking it. ## Reach a device you cannot route to If the device is registered in MK.IO, you can open its interface remotely rather than connecting over the local network. See [Access the remote device UI](/mkio/how-to/managing-edge-devices/access-the-remote-device-ui). ## Next step With a base URL that returns a system response, you can create your first channel. See [Create and manage channels](/api-guides/how-to/channels/manage-channels). # How Beam channels work A *channel* is a single end to end media path on an MK.IO Beam device: one or more inputs, an optional transform, and one or more outputs, managed as one object. The Advanced view of the device interface exposes the individual services that make this up. The Channels API, like the Essentials UI, hides that layer and gives you the channel as a single resource. Every channel is identified by `metadata.name`, which is also the value you put in the path of every request. `metadata.displayName` is the human readable label shown in the interface, and the two do not have to match. ## Spec and status A channel object has two halves, and knowing which one you are looking at saves a lot of confusion: | Half | Who writes it | What it holds | | :--- | :--- | :--- | | `spec` | You | The configuration you want: type, desired state, inputs, transform, and outputs. | | `status` | The device | What is actually happening: actual state, health severity, live input and output detail, and sync state. | `spec.state` is your *desired* state and accepts `Running` or `Stopped`. `status.state` is the *actual* state and reports the same two values. When you start a channel, the two disagree for a short period, which is normal. `GET /channels/{channel_id}/` returns both halves. A create or replace request sends only `kind`, `metadata`, and `spec`, because the device owns everything in `status`. ## The five channel types `spec.type` is the most important decision you make, because it determines which input and output types the device will accept and whether a transform is required. | `spec.type` | Use it for | Accepted inputs | Accepted outputs | | :--- | :--- | :--- | :--- | | `EncodingContribution` | Encoding a baseband source for onward contribution at high quality | `SDI`, `Smpte2110` | `ASI`, `UDP`, `SRTCaller`, `SRTListener`, `RF` | | `EncodingDistribution` | Encoding a baseband source for distribution to viewers | `SDI`, `Smpte2110` | `ASI`, `UDP`, `SRTCaller`, `SRTListener`, `RF` | | `EncodingStreaming` | Encoding a baseband source into adaptive bitrate streaming output | `SDI` | `HttpStreaming` | | `ReceptionDecoding` | Receiving a transport stream and decoding it back to baseband | `SRTCaller`, `SRTListener`, `UDP`, `ASI`, `SatDemod` | `SDI`, `Smpte2110` | | `ReceptionGateway` | Receiving a transport stream and passing it on without decoding | `SRTCaller`, `SRTListener`, `UDP`, `ASI`, `SatDemod` | `ASI`, `UDP`, `SRTCaller`, `SRTListener`, `RF` | `SDI` is a Serial Digital Interface port, `ASI` is an Asynchronous Serial Interface port, `SatDemod` is a satellite demodulator, `RF` is a satellite modulator output, and the `SRT` types are Secure Reliable Transport callers and listeners. Which of these a device actually has depends on its hardware, so read `GET /interfaces/` before you commit to a design. Contribution and distribution accept the same inputs and outputs, so the practical difference is the video codec list each one allows. Contribution offers the mezzanine grade codecs, including the 4:2:2 profiles and JPEG XS. Distribution offers the delivery grade codecs, such as `HEVCMain`, `H264High`, `H264Main`, and `MPEG2`. Choosing the wrong type is the most common reason a codec value is rejected. ## When a transform is required `spec.transform` carries the encoding configuration, and whether you must supply it depends on the channel type: - `EncodingContribution` and `EncodingDistribution` require `transform.encoding`, which holds the video settings and the audio track list. - `EncodingStreaming` requires `transform.abrEncoding` instead, which holds the adaptive bitrate representations. - `ReceptionDecoding` and `ReceptionGateway` do not take a transform at all, because nothing is re-encoded. ## Configuration is applied in the background Create, replace, and import requests return as soon as the device has accepted the channel. The device then pushes the underlying service configuration out separately, so a `200` response means the request was valid rather than that the channel is live. `status.syncState` tells you where that background work got to: | `syncState` | Meaning | | :--- | :--- | | `Ok` | The configuration on the device matches the spec you sent. | | `Configuring` | The device is still applying the change. | | `ConfigFailed` | The device rejected the configuration when it tried to apply it. | | `SyncFailed` | The device could not reconcile the channel with its services. | | `Failed` | The channel is in a failed state. | | `FieldError` | One or more fields could not be applied. | `status.syncError` is a list of strings that carries the detail behind the failure states. Poll `GET /channels/{channel_id}/` after any write and treat `syncState` as the real result of your request, not the status code. ## Health and alarms `status.health.severity` gives a single rolled-up severity for the channel, and the same severity scale appears on each input and output and on every alarm. The values are `Critical`, `Major`, `Minor`, `Notice`, `Ignore`, and `Clear`. A healthy channel reports `Clear`. Health tells you that something is wrong. Alarms tell you what. See [Monitor channels and devices](/api-guides/how-to/channels/monitor-channels) for reading both. ## Where to go deeper - [Beam Essentials UI](/beam/essentials) shows the same model through the device interface, which is often the quickest way to understand a channel before you script it. - [Manage alarms](/beam/system-admin/maintenance/manage-alarms) covers alarm types and overrides at the device level. - [Channels API reference](/api-reference/channels-api) lists every field of `ChannelSpec` and `ChannelStatus`. # Import and export channels Export and import move channel configuration in and out of a device as a single JSON payload. Use them to copy a working setup onto a second device, to keep a copy of a configuration before a risky change, or to rebuild a device to a known state. Both endpoints work on the whole payload rather than on one channel at a time, so they are a different job from the create and replace calls in [Create and manage channels](/api-guides/how-to/channels/manage-channels). ## Export a configuration `POST /export/` takes a body, even when you want everything. Omit `channels` to export every channel on the device: ```bash curl -X POST /export/ \ -H "Content-Type: application/json" \ -d '{ "description": "Studio A, before firmware update" }' \ --output beam-channels.json ``` To export a subset, name the channels: ```bash curl -X POST /export/ \ -H "Content-Type: application/json" \ -d '{ "channels": ["news-contribution", "udp-to-sdi"], "description": "News channels only" }' \ --output beam-channels.json ``` `description` is optional and is carried through into the exported payload, which makes it worth setting when the file is going to sit somewhere for a while. The payload identifies itself with `schemaVersion`, `exportedAt`, and `sourceVersion`, and holds the channels under `channels`. Each exported channel carries its `metadata` and `spec`, and by default also the underlying `services` that the device generated from that spec. Set `includeServices` to `false` for a lighter, specification-only export: ```bash curl -X POST /export/ \ -H "Content-Type: application/json" \ -d '{ "includeServices": false }' \ --output beam-channels-spec-only.json ``` On import, the device regenerates the services from its own templates. That is usually what you want when moving between devices, because the target device builds services suited to itself. Keep the services when you want the destination to match the source as closely as possible. ## Import a configuration `POST /import/` takes an `ExportPayload` under `payload`, along with the import mode. Send the payload you saved from the export step: ```bash curl -X POST /import/ \ -H "Content-Type: application/json" \ -d '{ "payload": , "mode": "merge" }' ``` `` is an object with the `schemaVersion`, `exportedAt`, and `channels` fields described above. Check it against the `ExportPayload` schema in the [Channels API reference](/api-reference/channels-api) if you are assembling one by hand rather than reusing an export. Two modes, and the difference matters: | `mode` | Effect | | :--- | :--- | | `merge` (default) | Creates channels that are missing, updates channels that have changed, and leaves every other channel on the device alone. | | `replace` | Deletes any channel that is not in the payload, then creates and updates the rest. | > **warning:** `mode` set to `replace` with an empty `channels` list deletes every channel on the device. Check the payload before sending a replace import. The optional `state` field overrides the desired state of every imported channel, and accepts `Running` or `Stopped`. Setting it to `Stopped` imports a configuration without putting anything on air, which is the safer way to bring a payload onto a live device. ## Check the result The response summarises what the device accepted: ```json { "imported": [ { "name": "news-contribution", "sourceName": "news-contribution", "displayName": "News contribution", "status": "accepted", "error": null } ], "deleted": [], "errors": [], "warnings": [] } ``` Each entry in `imported` has a `status` of `accepted`, `skipped`, or `rejected`, with `error` carrying the reason for a rejection. `deleted` lists the channels a replace import removed. `errors` and `warnings` cover problems with the payload as a whole rather than with one channel. `accepted` means the channel passed validation, not that it is configured. Import returns as soon as the payload is validated, and the device applies the configuration in the background. Poll `GET /channels/{channel_id}/` for each imported channel and read `status.syncState` to see how it finished. See [How Beam channels work](/api-guides/how-to/channels/how-channels-work) for the values. ## What goes wrong **A merge import did not remove an old channel.** That is what merge does. Use `replace` if the payload is meant to be the complete set. **Channels came up running when you did not want them to.** The desired state travels with the payload. Set `state` to `Stopped` on the import request to override it. **A channel was accepted but never came up.** Import validation and configuration are separate steps. Check `status.syncState` and `status.syncError` on the channel itself. **The destination device is equipped differently.** A spec that names `eth1` or `slot_1_port_1` depends on the target having that interface. Call `GET /interfaces/` on the destination and compare it against the payload before importing, then check both `ImportResult` and each channel's `status.syncState` afterwards. ## Where to go deeper - [Configuration backups](/mkio/how-to/managing-edge-devices/configuration-backups) covers whole-device backups through MK.IO, which is a broader safety net than a channel export. - [Channels API reference](/api-reference/channels-api) documents `ExportPayload`, `ImportRequest`, and `ImportResult` in full. # Create and manage channels Channels are created with `PUT /channels/{channel_id}/`, where `{channel_id}` is the channel name you choose. The same request creates a channel that does not exist and replaces one that does, so there is no separate create endpoint and no generated identifier to keep track of. Read [How Beam channels work](/api-guides/how-to/channels/how-channels-work) first if you have not picked a `spec.type` yet, because that choice constrains everything else in the body. ## Read the interfaces first Channel inputs and outputs point at real ports on the device, so start by asking the device what it has: ```bash curl /interfaces/ ``` The response groups interfaces by kind. This is a response from a VEGA-7010, abbreviated to the first two of its four slot ports, with the addresses replaced: ```json { "ip": [ { "name": "lo", "type": "IP", "ipAddresses": ["127.0.0.1"], "displayName": "lo" }, { "name": "eth0", "type": "IP", "ipAddresses": ["10.0.0.10"], "displayName": "eth0" }, { "name": "eth1", "type": "IP", "ipAddresses": ["172.16.0.10"], "displayName": "eth1" } ], "sdi": [ { "name": "slot_1_port_1", "type": "SDIASI", "url": "sdi://localhost/slot_1_port_1", "displayName": "Slot 1 / Port 1" }, { "name": "slot_1_port_2", "type": "SDIASI", "url": "sdi://localhost/slot_1_port_2", "displayName": "Slot 1 / Port 2" } ], "asi": [ { "name": "slot_1_port_1", "type": "SDIASI", "url": "asi://localhost/slot_1_port_1", "displayName": "Slot 1 / Port 1" }, { "name": "slot_1_port_2", "type": "SDIASI", "url": "asi://localhost/slot_1_port_2", "displayName": "Slot 1 / Port 2" } ], "rf_demod": [], "rf_modulator": [], "st2110": [] } ``` What a device returns depends entirely on the hardware fitted to it. The device above has no satellite demodulator or modulator, so `rf_demod` and `rf_modulator` are empty. Another device will list different slots, different port counts, and different groups, which is why this call belongs at the start of any integration rather than in your notes. Network transports take an interface `name`, such as `eth0`. Baseband and RF transports take the `url` exactly as returned. Copy the `url` rather than composing it. The same physical port is listed under both `sdi` and `asi` with the same `name`, and only the scheme in the `url` distinguishes them. A `name` on its own does not identify a port. Read the groups you need by key and ignore the rest, rather than assuming a fixed set. The groups a device returns can differ from the ones documented in the [Channels API reference](/api-reference/channels-api). ## Create a channel This request creates a contribution channel that takes a Serial Digital Interface (SDI) source, encodes it, and publishes it as a Secure Reliable Transport (SRT) listener that a remote decoder can connect to. It is created stopped, so nothing goes on air until you ask it to. ```bash curl -X PUT /channels/news-contribution/ \ -H "Content-Type: application/json" \ -d '{ "kind": "BeamChannel", "metadata": { "name": "news-contribution", "displayName": "News contribution" }, "spec": { "type": "EncodingContribution", "state": "Stopped", "inputs": [ { "type": "SDI", "name": "sdi-in", "transport": { "url": "sdi://localhost/slot_1_port_1" } } ], "transform": { "encoding": { "video": { "codec": "H264High", "videoFormat": "1920x1080p", "bitrate": 15000000 }, "audios": [ { "codec": "AC3", "mode": "Stereo", "bitrate": 128000 } ] } }, "outputs": [ { "type": "SRTListener", "transport": { "interface": "eth0", "port": 9000 } } ] } }' ``` The fields that are easy to get wrong: - Set `metadata.name` to the same value as the `{channel_id}` in the path. It is the identifier you use in every later request, and it cannot be changed afterwards. Any string works, so a readable slug such as `news-contribution` is easier to live with than a generated identifier. Channels created through the Essentials UI are named with a UUID instead, so expect both forms when you list a device you did not set up. - Each entry in `inputs` needs its own `name`. This is your label for the input, and it is how the audio entries in `transform.encoding.audios` refer back to it through `inputName`. The device assigns its own separate name to the input in `status`, so do not expect this value to appear there. - `bitrate` is in bits per second and accepts `100000` to `60000000`. Audio `bitrate` is also in bits per second, and the accepted values depend on the audio codec and mode. - `port` on an SRT listener accepts `256` to `65535`. - Outputs take no `name`, unlike inputs. The one exception is an `ASI` output, where `name` is optional. The device names every output itself in `status` regardless. The response is the full channel object, including the `metadata.created` timestamp and an initial `status`. A `200` here means the device accepted the configuration, not that the channel is configured. Confirm the result: ```bash curl /channels/news-contribution/ ``` Read `status.syncState`. It reports `Configuring` while the device applies the change and `Ok` once the running configuration matches your spec. Anything else means the configuration was accepted but could not be applied, and `status.syncError` carries the detail. ## Choosing between variants The body above is one shape of channel. Use this table to find the shape you need, then adapt the example: | Your situation | `spec.type` | Body differences | | :--- | :--- | :--- | | Encoding a baseband source for onward contribution | `EncodingContribution` | Requires `transform.encoding`. Video codecs are the 4:2:2 and JPEG XS profiles, plus `H264High` and its variants. | | Encoding a baseband source for delivery to viewers | `EncodingDistribution` | Requires `transform.encoding`. Video codecs are the delivery profiles: `HEVCMain`, `HEVCMain10bit`, `H264High`, `H264Main`, and `MPEG2`. | | Encoding a baseband source into adaptive bitrate output | `EncodingStreaming` | Requires `transform.abrEncoding` instead of `transform.encoding`. The only output type is `HttpStreaming`. | | Receiving a stream and putting it back to baseband | `ReceptionDecoding` | No `transform`. Outputs are `SDI` or `Smpte2110`. | | Receiving a stream and passing it on untouched | `ReceptionGateway` | No `transform`. Outputs are the transport types, as for contribution. | A reception channel is considerably shorter, because there is nothing to encode. This one takes a multicast UDP source and decodes it out of an SDI port, and starts immediately: ```bash curl -X PUT /channels/udp-to-sdi/ \ -H "Content-Type: application/json" \ -d '{ "kind": "BeamChannel", "metadata": { "name": "udp-to-sdi", "displayName": "UDP to SDI decoder" }, "spec": { "type": "ReceptionDecoding", "state": "Running", "inputs": [ { "type": "UDP", "name": "udp-in", "transport": { "url": "udp://239.100.1.1:5001", "interface": "eth0" } } ], "outputs": [ { "type": "SDI", "transport": { "url": "sdi://localhost/slot_1_port_1" } } ] } }' ``` A UDP input needs both a `url` and an `interface`, because the device has to know which network port to join the multicast group on. Set `unicast` to `true` if the source is a unicast stream. For the full list of input and output schemas, including SMPTE 2110 and satellite demodulator inputs, see the [Channels API reference](/api-reference/channels-api). ### Encrypting an SRT output To encrypt an SRT output, set `encryptionStandard` to `AES128`, `AES192`, or `AES256`, and supply a passphrase of at least 10 characters: ```json { "type": "SRTListener", "transport": { "interface": "eth0", "port": 9000, "encryptionStandard": "AES128", "passPhrase": "" } } ``` `passPhrase` is optional while `encryptionStandard` is `None`, which is the default, and required as soon as it is anything else. A passphrase shorter than 10 characters is rejected. ## Start, stop, and rename `PATCH /channels/{channel_id}/` handles the two changes you make most often. It accepts only the desired state and the display name, so use `PUT` for anything else. Start a channel: ```bash curl -X PATCH /channels/news-contribution/ \ -H "Content-Type: application/json" \ -d '{ "spec": { "state": "Running" } }' ``` Send `"Stopped"` in the same shape to take it off air. Rename it with the other half of the body: ```bash curl -X PATCH /channels/news-contribution/ \ -H "Content-Type: application/json" \ -d '{ "metadata": { "displayName": "News contribution, main feed" } }' ``` Both fields are nested under `spec` and `metadata` respectively. A flat body such as `{"state": "Running"}` does not change anything, because neither key is recognised at the top level. Renaming changes `displayName` only. `metadata.name` is fixed for the life of the channel, so a channel that needs a different identifier has to be recreated. ## Delete a channel ```bash curl -X DELETE /channels/news-contribution/ ``` A successful delete returns `204` with no body. The device stops the underlying services before removing the channel. Errors during that cleanup are logged on the device but do not stop the channel being removed, so a `204` confirms the channel is gone rather than that everything behind it shut down cleanly. ## What goes wrong **The request succeeded but nothing is on air.** Check `spec.state`. A channel created with `"state": "Stopped"` stays stopped until you patch it. If it is `Running`, check `status.syncState` and `status.syncError`. **A codec value is rejected.** The accepted video codecs depend on `spec.type`. `HEVCMain` is valid for `EncodingDistribution` and `EncodingStreaming` but not for `EncodingContribution`, which takes the 4:2:2 profiles. The accepted `videoFormat` values then depend on the codec you chose, and the interlaced formats are not available with the progressive-only codecs such as JPEG XS. **An audio bitrate is rejected.** Audio bitrate is constrained by codec and mode together. `AC3` with `Stereo` starts at `96000`, while `AC3` with `Surround51` starts at `224000`. **A `PUT` removed settings you did not send.** `PUT` replaces the channel rather than merging into it. Fetch the channel, change what you need in the returned `spec`, and send the whole `spec` back. **The response is `422`.** The body contains a `detail` array, and each entry has a `loc` array giving the exact path to the offending field. Read `loc` before anything else, because it points straight at the field name. **The response is `503`.** The channel endpoints return `503` when the API cannot serve the request. Retry, and if it persists, check the device itself. ## Where to go deeper - [Monitor channels and devices](/api-guides/how-to/channels/monitor-channels) covers reading status, alarms, and thumbnails once a channel exists. - [Beam Essentials UI](/beam/essentials) walks the same configuration through the channel creation wizard, which is a useful way to see a valid combination before you script it. - [Channels API reference](/api-reference/channels-api) holds the complete `ChannelSpec` schema, including every input and output variant. # Monitor channels and devices Everything you need to build a dashboard or an alerting rule comes from four places: the channel list, the alarm list, the thumbnail endpoint, and the device metrics. This page covers each of them and the query options that keep the responses small. ## List channels ```bash curl /channels/ ``` The response follows the same envelope as the other MK.IO list endpoints. Channels are in `value`, and `supplemental` carries the counts. This is a response from a device holding nine channels, requested one at a time with `$top=1`, with the addresses replaced: ```json { "value": [ { "kind": "BeamChannel", "metadata": { "name": "9b9c65bd-b383-4ed2-ae5a-fcde805e4e8c", "displayName": "demo-channel_0", "created": "2026-05-20T07:52:01.637000Z" }, "spec": { "type": "EncodingDistribution", "state": "Stopped", "inputs": [ { "type": "SDI", "name": "input_1", "transport": { "url": "sdi://localhost/slot_1_port_3" }, "payload": { "audios": [ { "name": "Audio_1", "aggregation": { "type": "None", "pair": "G1P1" } } ] } } ], "transform": { "encoding": { "video": { "codec": "H264Main", "videoFormat": "1280x720p", "bitrate": 2000000 }, "audios": [ { "name": "Audio_1_encoded_1", "inputName": "input_1", "audioName": "Audio_1", "codec": "PassThrough" } ] } }, "outputs": [ { "type": "UDP", "transport": { "url": "udp://239.1.1.1:5000", "interface": "eth1" } } ] }, "status": { "state": "Stopped", "health": { "severity": "Clear" }, "inputs": [ { "health": { "severity": "Clear" }, "name": "sdi-0", "thumbnailUrl": "thumbnails/sdi-0/thumbnail.jpg", "thumbnailPresence": "Absent", "type": "SDI" } ], "outputs": [ { "name": "udp-0", "health": { "severity": "Clear" }, "type": "UDP", "transport": { "url": "udp://239.1.1.1:5000", "interface": "eth1" }, "payload": { "video": { "resolution": "1280x720", "codec": "H264Main", "bitrate": 2000000 }, "audios": [{ "codec": "PassThrough" }] } } ], "syncState": "Ok", "syncError": [] } } ], "@odata.nextLink": "/channels?%24skiptoken=1&%24top=1&%24orderby=metadata%2Fcreated", "supplemental": { "kind": "BeamChannelList", "count": 1, "operation": "list", "pagination": { "start": 0, "end": 1, "records": 1, "total": 9 } } } ``` Three things in that response are worth pausing on. `metadata.name` is a UUID here because this channel was created in the Essentials UI. A channel you create through the API keeps whatever name you gave it. Both forms appear on the same device, so treat the name as an opaque identifier and show `displayName` to people. The names in `spec` and the names in `status` are not the same values. The spec input is called `input_1`, and the device reports it in `status` as `sdi-0`. Match inputs by position or by `type` rather than by name. `supplemental.pagination.records` counts what came back in this page, and `total` counts every channel that matched. Follow `@odata.nextLink` for the next page rather than calculating the offset yourself, and note that it comes back as a relative path with the `$` characters percent-encoded. An audio track encoded as `PassThrough` carries no `mode` or `bitrate`, because nothing is re-encoded. Those fields are required for every other audio codec. ## Filter, sort, and page The list endpoint takes OData-style query parameters: | Parameter | Default | Purpose | | :--- | :--- | :--- | | `$filter` | none | An expression that limits which channels are returned. | | `$orderby` | `metadata/created` | A sort field with optional `asc` or `desc`. Comma-delimit for multiple keys. | | `$select` | none | A comma-separated list of fields to return. | | `$top` | `1000` | Maximum items per page, from `1` to `1000`. | | `$skiptoken` | `0` | Offset into the results. | `$filter` supports the operators `eq`, `ne`, `lt`, `le`, `gt`, and `ge`, combined with `and` and `or`, plus the functions `contains()`, `tolower()`, and `toupper()`. Paths into the object use slashes. Fetch only the channels that are not healthy, worst first: ```bash curl -G /channels/ \ --data-urlencode "\$filter=status/health/severity ne 'Clear'" \ --data-urlencode "\$orderby=status/health/severity desc" ``` Escaping the `$` matters in shells that would otherwise treat `$filter` as a variable, and `--data-urlencode` handles the spaces and quotes in the expression for you. A few more expressions that cover most monitoring needs: - `$filter=status/state eq 'Running'` returns the channels that are actually on air, which is not the same as the channels you asked to run. - `$filter=contains(tolower(metadata/displayName), 'news')` does a case-insensitive name search. - `$filter=metadata/created ge 2026-01-01T00:00:00Z` returns recently created channels. Filtering, sorting, and pagination are all applied on the device after every channel has been loaded with its live status. That is fine at the channel counts a single Beam device carries, but it does mean a narrow `$filter` does not make the call cheaper. `$select` is the exception. When you pass it, the device skips loading live status entirely, so a call that only needs names and types is noticeably lighter than a full list. ## Read one channel ```bash curl /channels/news-contribution/ ``` The `status` object is where the live detail sits: - `state` is the actual state, `Running` or `Stopped`, which can differ from `spec.state` while a change is being applied. - `health.severity` is the rolled-up channel severity: `Critical`, `Major`, `Minor`, `Notice`, `Ignore`, or `Clear`. - `inputs` and `outputs` carry their own health, along with transport detail such as SRT connection statistics and the services detected on the input. - `syncState` and `syncError` report whether the configuration you sent was successfully applied. See [How Beam channels work](/api-guides/how-to/channels/how-channels-work) for the values. ## Read active alarms Health tells you a channel is unwell. Alarms tell you why. The alarm endpoint is scoped to one channel, and `channelName` is required: ```bash curl -G /channelalarms/active/ \ --data-urlencode "channelName=news-contribution" ``` Each alarm has a `metadata` half identifying it and a `status` half describing it. The placeholders below stand in for values the device supplies: ```json { "value": [ { "kind": "Alarm", "metadata": { "id": "", "name": "", "displayName": "", "objectId": "", "channelName": "news-contribution", "created": "2026-07-01T09:31:12Z" }, "status": { "severity": "Critical", "additionalInfo": "" } } ], "supplemental": { "kind": "AlarmList", "count": 1, "operation": "list", "pagination": { "start": 0, "end": 1, "records": 1, "total": 1 } } } ``` `` is a UUID, and `` is the device's machine name for the fault, with `` its readable form. `` identifies the part of the channel that raised it. `` is the free-text explanation in `status.additionalInfo`, and it is the field worth surfacing in an alert. `severity` uses the same scale as channel health. For the alarms a Beam device raises and how to override them, see [Manage alarms](/beam/system-admin/maintenance/manage-alarms). Because the endpoint takes one channel at a time, an alarm view across a device means listing the unhealthy channels first and then fetching alarms for each one. ## Fetch an input thumbnail Each entry in `status.inputs` carries a `thumbnailUrl`, relative to the channel: ```json { "name": "sdi-0", "thumbnailUrl": "thumbnails/sdi-0/thumbnail.jpg", "thumbnailPresence": "Absent", "type": "SDI" } ``` Use that value rather than building the path yourself. The identifier in it is the name the device assigns to the input in `status`, not the name you gave the input in `spec`, so composing the path from your own spec produces a URL that does not resolve. ```bash curl /channels//thumbnails/sdi-0/thumbnail.jpg \ --output thumbnail.jpg ``` Check `thumbnailPresence` before you fetch. It reports `Present`, `Absent`, `Unknown`, or `Unsynced`. The path ends in `.jpg` so that browsers treat the response as a plain image, which means you can point an `` tag straight at it. ## Query device metrics For a point-in-time reading of the device itself: ```bash curl /metrics/current/ ``` ```json { "data": { "serverId": "Server1", "cpuUtilizationPercent": 34.52094163392065, "uptimeSeconds": 1041534.8420000076 }, "collectedAt": "2026-07-29T13:13:32.843368Z" } ``` `temperatureCelsius` is not present in the response above. Read every field under `data` defensively: depending on the device and the sensors fitted to it, a metric can come back as `null` or be left out of the response altogether. Show a dash where a value is absent, as the Essentials dashboard does. Narrow the response with `$select`, which accepts `serverId`, `cpuUtilizationPercent`, `temperatureCelsius`, and `uptimeSeconds`: ```bash curl -G /metrics/current/ \ --data-urlencode "\$select=cpuUtilizationPercent,temperatureCelsius" ``` For a history rather than a snapshot, `GET /metrics/cpu/` and `GET /metrics/temperature/` take a time range. Both require `start` and `end`, which accept ISO 8601 timestamps or Unix timestamps, and take an optional `step` that defaults to `60s`: ```bash curl -G /metrics/cpu/ \ --data-urlencode "start=2026-07-01T00:00:00Z" \ --data-urlencode "end=2026-07-01T01:00:00Z" \ --data-urlencode "step=60s" ``` These return the Prometheus range format rather than the envelope used elsewhere in this API. `status` is `success` or `error`, and `data.result` is an array of series, each with a `metric` label set and a `values` array of `[unix_timestamp, value_string]` pairs. The value is a string, so parse it before you chart it. `GET /metrics/query/` takes an arbitrary PromQL expression in `query`, along with the same `start`, `end`, and `step`. Use it when the two named endpoints do not cover the metric you need. ## What goes wrong **The channel list is slow.** Live status is loaded for every channel before filtering, so the cost is in the channel count rather than the filter. If you are polling frequently and only need identity, pass `$select` to skip status hydration. **A `$filter` returns nothing you expected.** String values in an expression are single quoted, and paths use slashes rather than dots. Compare `status/health/severity ne 'Clear'` against a dotted or unquoted version, which will not match. **A thumbnail URL does not resolve.** The identifier in the path is the input name from `status`, which the device assigns, not the input name you set in `spec`. Read `thumbnailUrl` off the status object instead of composing the path. **A metric field has no value.** A reading the device cannot supply can be `null` or absent from `data`, so a client that reads `data.temperatureCelsius` directly may get either on a device without a temperature sensor. Treat every field under `data` as optional and handle both. ## Where to go deeper - [Manage alarms](/beam/system-admin/maintenance/manage-alarms) covers alarm types and overrides at the device level. - [Beam Essentials UI](/beam/essentials) shows the same status, alarms, and metrics on the device dashboard. - [Channels API reference](/api-reference/channels-api) documents every field of `ChannelStatus`, including the per-transport statistics. # Build with the Fleets API The Fleets API is the operational API for MK.IO Beam devices. Use it to register devices into a project, protect their configuration with backups, manage software versions, and collect support packages when you need diagnostics. ## API overview | Detail | Value | | --- | --- | | Base path | `/api/v1/projects/{project_name}/fleet/` | | Scope | Project | | Reference | [Fleets API reference](/api-reference/fleets-api) | ## How the main resources connect The Fleets API revolves around a few resource types: 1. A **device** represents the Beam device record in MK.IO. 2. A **backup** is a configuration snapshot you can restore later. 3. **Software** settings on the device control desired and preloaded versions. 4. A **support package** is a diagnostic bundle generated on the device and uploaded to storage. That means a typical operational flow is: register the device, take backups before risky changes, control software state through device patch operations, and collect support packages when something goes wrong. ## Common workflow patterns ## Guides - [Manage devices](/api-guides/how-to/fleets/device-management): Register devices, inspect state, and work through the core day-to-day operations. - [Backups and restores](/api-guides/how-to/fleets/backups-and-restores): Create backups before changes, inspect stored backup metadata, and restore safely. - [Software and updates](/api-guides/how-to/fleets/software-and-updates): Control desired and preloaded software versions and collect support packages for diagnostics. # Backups and restores A backup is the safety net for Beam device operations. Take one before a software change or a service removal, and you have a configuration you can roll back to. A backup captures the device's services, templates, failover groups, server definitions, and local users. Backup and restore operations run asynchronously on the device and return `202 Accepted`. ## Create a backup A backup is triggered with `POST` against the device. Use a descriptive `backupName`, such as one with a date, so its purpose is clear later. ```bash curl -X POST "https://app.mk.io/api/v1/projects//fleet/devices/my-device/backup" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "metadata": { "displayName": "Pre-update backup" }, "spec": { "backupName": "pre-update-20260415-100000", "backupAssetFile": false } }' ``` The call returns `202 Accepted` and the backup completes on the device over the next while. Configuration access on the device is blocked while a backup or restore is in progress. ## Browse backups List the backups in a project, and read one to get its stored file details: ```bash curl -X GET "https://app.mk.io/api/v1/projects//fleet/backups" \ -H "Authorization: Bearer " curl -X GET "https://app.mk.io/api/v1/projects//fleet/backups/" \ -H "Authorization: Bearer " ``` A backup record carries its `timestamp`, the `blobStorageUrl` of the stored file, and a `signature` for integrity verification. The list endpoint supports `$filter`; see the [Fleets API reference](/api-reference/fleets-api) for the filterable fields. ## Restore a backup Restore is also a `POST`, with the stored file path from the backup record. It returns `202 Accepted` and applies asynchronously. ```bash curl -X POST "https://app.mk.io/api/v1/projects//fleet/devices/my-device/restore" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "metadata": { "displayName": "Restore pre-update snapshot" }, "spec": { "restoreFilePath": "my-device//pre-update-20260415-100000.tar.gz", "restoreAssetFile": false } }' ``` Confirm the backup identity before you restore it onto a live device. ## Upload an external backup If you already hold a `.tar.gz` backup file, upload it and let MK.IO generate its metadata: ```bash curl -X POST "https://app.mk.io/api/v1/projects//fleet/uploadBackup" \ -H "Authorization: Bearer " \ -H "Content-Type: application/octet-stream" \ --data-binary @my-device-backup.tar.gz ``` ## What goes wrong - **Restoring a backup onto a different Beam server.** This is not supported. A backup restores to the device it came from. See the on-device detail in [Backup and restore](/beam/system-admin/maintenance/backup-restore). - **Old backups disappearing.** A device keeps up to 30 local backups and deletes the oldest when that limit is reached. Download or upload any you must retain. - **Configuration changes blocked mid-operation.** Device configuration access is locked while a backup or restore runs. Wait for it to finish. ## What comes next - [Software and updates](/api-guides/how-to/fleets/software-and-updates): take a backup before changing software state. - [Manage devices](/api-guides/how-to/fleets/device-management): return to the device once protection is in place. # Manage devices Device management is where most Fleets API workflows begin. MK.IO Beam is an on-premises media processing appliance, and the Fleets API is the cloud control plane for it. You register a device into a project, give it a site and the networks it can use, then move into backup, software, or diagnostic operations. For the on-device onboarding steps that precede registration, see [Onboard a fleet device](/mkio/how-to/managing-edge-devices/on-board-fleet-devices/api-onboarding). ## Register a device A device is created with `PUT`, using its name in the URL. You associate the physical appliance with this record using either a `shortCode` or a `locationId` read from the device web interface. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//fleet/devices/beam-encoder-01" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "metadata": { "displayName": "Beam Encoder 01", "labels": { "location": "studio-a", "role": "encoder" } }, "spec": { "shortCode": "XU7U23WD", "siteName": "headquarters", "availableNetworks": [] } }' ``` The fields that matter: - `shortCode`: the 8-character code from the device web interface. It rotates frequently, so use it promptly. - `locationId`: a stable device identifier that does not rotate. Prefer it for scripted, large-scale onboarding. - `siteName`: the site the device belongs to. See [Set up networks and sites](/api-guides/how-to/infrastructure/networks-and-sites). - `availableNetworks`: the networks the device may use. An empty array gives it access to every network with a route to its site. After registration, the device web interface shows **Registered: Yes**. ## Read the device and its status ```bash curl -X GET "https://app.mk.io/api/v1/projects//fleet/devices/beam-encoder-01" \ -H "Authorization: Bearer " ``` The `status` block is where you watch the device's health and software. The fields worth tracking: | Field | Tells you | | :--- | :--- | | `currentSoftwareVersion` | The version running now. | | `softwareUpgradeState` | `Idle`, `Switching`, or `SwitchFailed` during a version change. | | `alarmSeverity` | `0` Clear, `1` Info, `2` Warning, `3` Error, `4` Critical. | | `lastContact` | When the device last reached the cloud. | The list endpoint supports `$filter`, `$orderby`, `$top`, `$skiptoken`, and label queries, including sorting on `status/alarmSeverity` and `status/currentSoftwareVersion`. ## Update device settings Use `PATCH` to change settings such as the site, networks, or software preferences: ```bash curl -X PATCH "https://app.mk.io/api/v1/projects//fleet/devices/beam-encoder-01" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "autoPreloadLatestSoftware": true } }' ``` The software-related fields (`desiredSoftwareVersion`, `preloadSoftwareVersion`, `autoPreloadLatestSoftware`) are covered in [Software and updates](/api-guides/how-to/fleets/software-and-updates). ## What goes wrong - **Registration fails because the short code expired.** Short codes rotate. Keep the device interface open so it shows the current code, or register with the non-rotating `locationId` instead. - **Deleting a device returns `409 Conflict`.** The device is still referenced, for example by an assigned flow. Clear the dependency, then retry the delete. ```bash curl -X DELETE "https://app.mk.io/api/v1/projects//fleet/devices/beam-encoder-01" \ -H "Authorization: Bearer " ``` ## What comes next - [Backups and restores](/api-guides/how-to/fleets/backups-and-restores): protect device state before major changes. - [Software and updates](/api-guides/how-to/fleets/software-and-updates): control desired and preloaded software versions. # Software and updates Software management in the Fleets API is device-centric. You inspect the versions available to a device, then set fields on the device resource to choose what runs or preloads. The same surface produces support packages for diagnostics. For the product background, see [Update device software](/mkio/how-to/managing-edge-devices/update-device-software). ## List available software ```bash curl -X GET "https://app.mk.io/api/v1/projects//fleet/devices/my-device/software" \ -H "Authorization: Bearer " ``` Each entry reports its `version` and whether the device can move to it, through `status.upgradeCompatible` and `status.downgradeCompatible`. Check those before you target a version. ## Choose how the update happens Three device-spec fields control software, and the right one depends on whether you want to apply the change now or stage it for later. | To... | Set | Effect | | :--- | :--- | :--- | | Run a specific version now | `desiredSoftwareVersion` | The device switches to it. `softwareUpgradeState` moves `Switching` then `Idle`. | | Stage a version without applying it | `preloadSoftwareVersion` | The device downloads it; watch `status.preloadedSoftware` for progress. | | Always keep the newest staged | `autoPreloadLatestSoftware` | The device preloads the latest available version automatically. | Set the running version: ```bash curl -X PATCH "https://app.mk.io/api/v1/projects//fleet/devices/my-device" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "desiredSoftwareVersion": "1.2.3.4" } }' ``` Preloading first, then setting the desired version once the download is complete, makes the actual switch fast and predictable. ## Support packages A support package is the main diagnostic bundle. List the packages held for a device (up to five, newest first), and trigger a new one: ```bash curl -X GET "https://app.mk.io/api/v1/projects//fleet/devices/my-device/supportPackages" \ -H "Authorization: Bearer " ``` ```bash curl -X POST "https://app.mk.io/api/v1/projects//fleet/devices/my-device/supportPackages" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "metadata": { "displayName": "Audio sync issue" }, "spec": { "description": "Investigating audio sync drift on the output stream", "name": "audio-sync-issue-20260415", "collectors": ["Logs", "Host"] } }' ``` Only `description` is required. Omit `collectors` to include all of them. The call returns `202 Accepted`, and only one support-package operation can run on a device at a time. ## Remove selected services To remove services from a device, select them by tag. Take a backup first if you might need to restore the current configuration. ```bash curl -X POST "https://app.mk.io/api/v1/projects//fleet/devices/my-device/removeServices" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "description": "Remove audio processing services", "selector": { "tags": { "include": ["audio"], "exclude": [] } } } }' ``` ## What goes wrong - **An "upgrade" that becomes a rollback.** Beam devices use a dual-bank mechanism. If the version you set matches the version already in the second bank, the device performs a rollback rather than a fresh upgrade. Confirm the target version is the one you intend. - **A preloaded version that vanished.** Software-bundle storage is limited, and the device deletes older bundles automatically to make room. Preload close to when you plan to apply. - **A support package request rejected.** Only one support-package operation runs per device at a time. Wait for the current one to finish. ## What comes next - [Backups and restores](/api-guides/how-to/fleets/backups-and-restores): create the rollback point before a software change. - [Manage devices](/api-guides/how-to/fleets/device-management): return to the device resource and its site and network settings. # Build with the Infrastructure API The Infrastructure API defines where Beam devices live and which networks they can reach. Use it to create networks first, then create sites with routes that reference those networks, and finally register devices against those sites through the Fleets API. ## API overview | Detail | Value | | --- | --- | | Base path | `/api/v1/projects/{project_name}/infra/` | | Scope | Project | | Reference | [Infrastructure API reference](/api-reference/infrastructure-api) | ## The two core resources ### Networks Networks represent logical network segments. Their status includes: - `status.owner` - `status.scope` These are valid sort and filter fields for the network list endpoint. ### Sites Sites represent locations where devices operate. A site can include routes that reference one or more networks, which is why site creation usually comes after network creation. ## Typical sequence 1. List existing networks and sites. 2. Create the networks you need. 3. Create sites with routes that reference those networks. 4. Register Beam devices to those sites through the Fleets API. 5. Remove dependent references before deleting sites or networks. ## Common workflow patterns ## Guide - [Set up networks and sites](/api-guides/how-to/infrastructure/networks-and-sites): Follow the recommended order for creating and maintaining infrastructure resources. # Set up networks and sites Networks and sites are the topology layer for Beam devices. A network is a logical segment; a site is a location where devices operate, and its routes decide which networks those devices can reach. The order matters: create the networks first, then create sites whose routes reference them, then register devices to the sites through the [Fleets API](/api-guides/how-to/fleets/device-management). ## Create a network A network is created with `PUT`. Its `spec` is currently empty but must be present; the meaningful content is in `metadata`. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//infra/networks/production-network" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "metadata": { "displayName": "Production Network", "labels": { "environment": "production" } }, "spec": {} }' ``` Reading a network back shows its `status.owner` (`User` or `System`) and `status.scope` (`Connecting` between sites, or `Local` within one). System-owned and local networks are created by the platform. ## Create a site with routes Once the network exists, create a site whose `routes` reference it. A route requires only `networkName`. You can add an optional `defaultTransport` to set how content moves on that network. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//infra/sites/headquarters" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "metadata": { "displayName": "Headquarters", "labels": { "region": "europe", "type": "primary" } }, "spec": { "routes": [ { "networkName": "production-network", "defaultTransport": { "type": "SRTListener" } } ] } }' ``` `defaultTransport.type` is one of `Auto`, `SRTListener`, `SRTCaller`, `UDP`, or `RISTListener`. A device assigned to this site (through its `siteName`) automatically reaches the networks the site routes to. ## Update and delete `PATCH` updates labels, display name, or a site's routes. A `PATCH` to `routes` replaces the whole array, so send the complete set. ```bash curl -X PATCH "https://app.mk.io/api/v1/projects//infra/sites/headquarters" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "metadata": { "displayName": "Main Headquarters" } }' ``` ## What goes wrong - **Creating a site before its network exists.** A route references a network by name. Create the networks first. - **A delete returns `409 Conflict`.** A network cannot be deleted while a site routes to it, and a site cannot be deleted while devices or flows reference it. Clear those references first. - **Trying to delete a system-owned resource.** System-owned networks and sites cannot be deleted, though they can be modified. Check `status.owner` before you try. ## What comes next - [Manage devices](/api-guides/how-to/fleets/device-management): register Beam devices against the sites you created. # Build with the Management API The Management API is the control plane for MK.IO. Use it when you need to create projects, manage users and teams, issue tokens, assign payment methods, enable metrics, or configure webhook rules that other workflows depend on. ## API overview | Detail | Value | | --- | --- | | Base path | `/api/v1/` | | Scope | Organization and project | | Reference | [Management API reference](/api-reference/management-api) | ## How the Management API fits into other workflows You often use this API before you use anything else: 1. Create a project and assign billing. 2. Grant access to the right users and teams. 3. Create or rotate tokens for automation. 4. Add webhook rules so media or fleet workflows can notify your application. That makes the Management API the setup and governance layer for the rest of the platform. ## Common workflow patterns ## Guides - [Users and teams](/api-guides/how-to/management/users-and-teams): Understand users, invites, teams, roles, scopes, and the JSON Patch operations used to evolve team access. - [Tokens](/api-guides/how-to/management/tokens): Create full-access or restricted tokens, inspect token metadata, and revoke user or organization tokens. - [Organizations and invites](/api-guides/how-to/management/organizations-and-invites): Inspect accessible organizations, accept pending invitations, and leave or decline organization access cleanly. - [Webhook rules](/api-guides/how-to/management/webhooks): Register webhook destinations, choose events, and inspect event delivery status for each rule. - [Usage and billing](/api-guides/how-to/management/usage-and-billing): Inspect current project usage, run date-range reports, assign payment methods, and enable metrics export. ## Workflow guide - [Provision org and users](/api-guides/how-to/management/org-provisioning): Create a project, assign billing, then move into access and token setup for the people and systems that will use it. # Provision org and users This guide covers the setup that happens first in a new MK.IO environment: confirm the organization, create a project, assign billing, then prepare access for the people and automation that will use it. Each resource here has its own detailed guide; this page is the order to do them in. A project is the container that holds your media objects, and each project and payment method belongs to exactly one organization. So the sequence is always organization, then billing, then project, then access. ## Step 1: Confirm or create the organization Check which organization the current token belongs to: ```bash curl -X GET "https://app.mk.io/api/v1/organization" \ -H "Authorization: Bearer " ``` If the user needs a new organization, create one. Only `name` is required; the legal-entity fields are optional. ```bash curl -X POST "https://app.mk.io/api/v1/organization" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "name": "Example Media Organization", "legalEntityName": "Example Media Ltd", "legalEntityContactEmail": "legal@example.com" } }' ``` ## Step 2: Pick a payment method A project cannot be created without a payment method, so list the ones available first: ```bash curl -X GET "https://app.mk.io/api/v1/organization/paymentMethods" \ -H "Authorization: Bearer " ``` If the chosen method requires terms acceptance, accept them before assigning it: ```bash curl -X POST "https://app.mk.io/api/v1/organization/paymentMethods//acceptTermsAndConditions" \ -H "Authorization: Bearer " ``` ## Step 3: Create the project A project is created with `PUT`, using the name in the URL. It requires `displayName`, `locationName`, and `paymentMethodId`. ```bash curl -X PUT "https://app.mk.io/api/v1/projects/production-media" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "displayName": "Production Media", "locationName": "westeurope", "paymentMethodId": "" }' ``` Assigning a payment method also activates the project. You can read or replace the assignment later through `/projects//paymentMethod`; see [Usage and billing](/api-guides/how-to/management/usage-and-billing). ## Step 4: Prepare access Review the users, roles, and scopes the organization already has, which are the inputs for team setup: ```bash curl -X GET "https://app.mk.io/api/v1/organization/users" \ -H "Authorization: Bearer " curl -X GET "https://app.mk.io/api/v1/organization/roles" \ -H "Authorization: Bearer " curl -X GET "https://app.mk.io/api/v1/organization/scopes" \ -H "Authorization: Bearer " ``` Then create a team that grants roles under a scope. See [Users and teams](/api-guides/how-to/management/users-and-teams) for the full access model and the JSON Patch operations used to evolve a team. ```bash curl -X PUT "https://app.mk.io/api/v1/organization/teams/video-engineering" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "description": "Team for day-to-day project operations", "members": {}, "scopes": { "": { "roles": [""] } } } }' ``` ## Step 5: Create automation tokens Create a token for the systems that will call the APIs. Prefer a `restricted` token for automation so it cannot do more than the job needs. See [Tokens](/api-guides/how-to/management/tokens). ```bash curl -X POST "https://app.mk.io/api/v1/user/tokens" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "type": "restricted", "description": "Project automation token", "organizationId": "", "permissions": {} }' ``` ## Step 6: Add webhook rules early If the project will run jobs or live workflows, add [webhook rules](/api-guides/how-to/management/webhooks) at the start rather than waiting until polling becomes painful. That lets your application react to job completion, live channel state changes, and locator creation from day one. ## What goes wrong - **Creating a project before billing exists.** A project needs a `paymentMethodId`, and some methods need terms accepted first. Do Step 2 before Step 3. - **Assigning everything to individual users.** Grant roles to teams under scopes, then add users to teams. It is reusable and far easier to audit. ## What comes next - [Users and teams](/api-guides/how-to/management/users-and-teams): roles, scopes, and JSON Patch in detail. - [Tokens](/api-guides/how-to/management/tokens): choose token types and inspect token metadata. - [Build with the Media API](/api-guides/how-to/media): move from setup into media workflows. # Organizations and invites Organization membership has two sides. An admin invites people to an organization and can cancel a pending invite. A user accepts or declines invitations, lists the organizations they can reach, and leaves an organization. The two use different endpoint groups: `/organization/invites` for the admin side, and `/user/organizations` for the user side. ## Invite a user (admin) Create a pending invitation by email. A `comment` explains why, and `teams` adds the user to those teams when they accept. ```bash curl -X POST "https://app.mk.io/api/v1/organization/invites" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "email": "jane.smith@example.com", "comment": "Video encoding specialist for the Delphi project", "teams": ["encoding-team"] } }' ``` The invited user receives an email with an invitation link. Two things are worth knowing: an invitation **expires after 14 days**, and an accepted user **automatically joins the Everyone team** in addition to any teams you name. List pending invites, and cancel one that is no longer needed: ```bash curl -X GET "https://app.mk.io/api/v1/organization/invites" \ -H "Authorization: Bearer " curl -X DELETE "https://app.mk.io/api/v1/organization/invites/" \ -H "Authorization: Bearer " ``` ## List the organizations a user can reach From the user side, list every organization the current user belongs to, plus any they have a pending invite to: ```bash curl -X GET "https://app.mk.io/api/v1/user/organizations" \ -H "Authorization: Bearer " ``` If an organization record carries an `invite` object, the user has not joined it yet. The `invite` includes who sent it, a comment, and a `state`. ## Accept an invitation Accepting is a `PATCH` on the user's organization record. The invite `state` is the only field you can change; set it to `accepted` to join. ```bash curl -X PATCH "https://app.mk.io/api/v1/user/organizations/" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "invite": { "state": "accepted" } } }' ``` ## Decline an invite or leave an organization `DELETE` on the same resource handles both, depending on the current relationship: it declines a pending invite, or removes a current member from the organization. ```bash curl -X DELETE "https://app.mk.io/api/v1/user/organizations/" \ -H "Authorization: Bearer " ``` Leaving is only reversible by accepting a fresh invitation, so confirm before you call it. ## What goes wrong - **An invite that is never accepted in time.** Invitations expire after 14 days. Re-send if the window passes. - **Using the wrong endpoint group.** Use `/organization/invites` when an admin manages invitations for others, and `/user/organizations` when the current user accepts, declines, or leaves. They are not interchangeable. ## What comes next - [Users and teams](/api-guides/how-to/management/users-and-teams): the team, role, and scope model new members are granted access through. - [Provision org and users](/api-guides/how-to/management/org-provisioning): the full setup sequence from organization to access. # Tokens Tokens authenticate applications to the MK.IO APIs. The Management API gives you two views: user-level management for your own tokens, and organization-level management for auditing or revoking tokens across the organization. A token never grants more than the user who created it has, so the main decision is how much of that access to expose. ## Choosing a token type There are four types. For automation, prefer `restricted` so an exposed token cannot do more than its job requires. | Type | Capabilities | Expiry | | :--- | :--- | :--- | | `login` | Full user capabilities | Short-lived | | `full-access` | Full user capabilities | Optional, up to one year | | `restricted` | A scoped subset you define | Optional, up to one year | | `ephemeral` | A scoped subset, short-lived | Short-lived | ## Create a full-access token A token is created with `POST`. `type` and `organizationId` are always required. ```bash curl -X POST "https://app.mk.io/api/v1/user/tokens" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "type": "full-access", "description": "CI deployment token", "organizationId": "", "expireDate": "2027-01-01T00:00:00Z" }' ``` The response includes the generated token as `metadata.JWT`. MK.IO does not store it, so it is **visible only at creation time, for five minutes**. Copy it somewhere secure immediately. ## Create a restricted token A `restricted` token adds a `permissions` object that must be a strict subset of your own capabilities. Read your access first so you know what you can grant: ```bash curl -X GET "https://app.mk.io/api/v1/user/rbac" \ -H "Authorization: Bearer " ``` Then build `permissions` from a subset of that structure. This example grants only asset operations on one project: ```bash curl -X POST "https://app.mk.io/api/v1/user/tokens" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "type": "restricted", "description": "Asset automation, one project", "organizationId": "", "expireDate": "2027-01-01T00:00:00Z", "permissions": { "core.project": { "": { "ams.asset": ["create", "delete", "get", "update"] } } } }' ``` ## Inspect and revoke List or read your own tokens. Expired tokens are filtered out of the list. ```bash curl -X GET "https://app.mk.io/api/v1/user/tokens" \ -H "Authorization: Bearer " ``` A token record carries operational fields including `issued`, `expires`, `lastUsed`, `revoked`, and (for restricted tokens) `permissions`. Revoke one token, or all of your tokens at once: ```bash curl -X DELETE "https://app.mk.io/api/v1/user/tokens/" \ -H "Authorization: Bearer " ``` ```bash curl -X DELETE "https://app.mk.io/api/v1/user/tokens" \ -H "Authorization: Bearer " ``` Use the revoke-all carefully: it affects all of your tokens across organizations. ## Audit tokens across the organization An organization admin can list every token with access to the organization, and revoke any of them: ```bash curl -X GET "https://app.mk.io/api/v1/organization/tokens" \ -H "Authorization: Bearer " curl -X DELETE "https://app.mk.io/api/v1/organization/tokens/" \ -H "Authorization: Bearer " ``` ## What goes wrong - **The token is lost after creation.** The `JWT` is shown once, for five minutes, and is never stored. If you miss it, revoke the token and create a new one. - **A restricted token returns `403`.** Its `permissions` must be a subset of your own access. Compare against `/api/v1/user/rbac`, and remember the token loses capabilities if your own access is later reduced. - **Forgetting the expiry ceiling.** `expireDate` cannot be more than one year after creation. ## What comes next - [Users and teams](/api-guides/how-to/management/users-and-teams): the access model a token's permissions draw from. - [Authentication and tokens](/api-guides/getting-started/authentication): bearer-auth basics and UI-based personal token creation. # Usage and billing The Management API offers two reporting views. Project usage gives the current billing-month view for one project. Organization usage reports query a custom date range across selected projects or payment methods. The same surface manages payment methods and project metrics export. MK.IO bills on metered, pay-as-you-go usage, so these reports map directly to what you are charged. ## Current project usage For a quick month-to-date view of one project: ```bash curl -X GET "https://app.mk.io/api/v1/projects//usage" \ -H "Authorization: Bearer " ``` This returns usage since the start of the current month, grouped by meter. ## Date-range usage reports For any other range, post a query to the reporting endpoint. `startDate` is inclusive and `endDate` is exclusive, both as `YYYY-MM-DD` in UTC. ```bash curl -X POST "https://app.mk.io/api/v1/organization/reports/usage" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "startDate": "2026-01-01", "endDate": "2026-02-01", "granularity": "day", "projectNames": ["my-project"] }' ``` The query supports: - `granularity`: `hour`, `day`, `week`, `month`, or `year`. The range is capped by granularity: 31 days for `hour`, one year for `day`, and unbounded for `week` and above. - `projectNames` or `paymentMethodIds` to filter. These two are mutually exclusive. - `format`: `json` (default) or `csv`. - `download`: set `true` to return the report as a file download. - `filterOnReportedDate`: set `true` to filter by when usage was charged rather than when it occurred, which helps reconcile against invoice dates. To pull a CSV for finance: ```bash curl -X POST "https://app.mk.io/api/v1/organization/reports/usage" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "startDate": "2026-01-01", "endDate": "2026-02-01", "format": "csv", "download": true, "filterOnReportedDate": true }' ``` ## Payment methods List the organization's payment methods, and read one method's rate card to see per-meter prices: ```bash curl -X GET "https://app.mk.io/api/v1/organization/paymentMethods" \ -H "Authorization: Bearer " curl -X GET "https://app.mk.io/api/v1/organization/paymentMethods//rateCard" \ -H "Authorization: Bearer " ``` Read or replace the method assigned to a project: ```bash curl -X GET "https://app.mk.io/api/v1/projects//paymentMethod" \ -H "Authorization: Bearer " curl -X POST "https://app.mk.io/api/v1/projects//paymentMethod" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "paymentMethodId": "" }' ``` ## Enable metrics export Turn on the project metrics endpoint with a `PATCH`. When enabled, the read response returns the connection details (`url`, `username`, `password`) for scraping metrics. ```bash curl -X PATCH "https://app.mk.io/api/v1/projects//metricsEndpoint" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "enabled": true } }' ``` ## What goes wrong - **A payment method seems stuck on a project.** A method cannot be removed, only replaced with another. Assigning one also activates the project. - **An hourly report is rejected for too wide a range.** `hour` granularity is limited to 31 days. Use `day` or coarser for longer ranges. - **Report totals do not match an invoice.** The report defaults to when usage occurred. Set `filterOnReportedDate` to `true` to align with billing dates. ## What comes next - [Provision org and users](/api-guides/how-to/management/org-provisioning): set up projects before reporting on them. - [Management API reference](/api-reference/management-api): the full reporting and billing schemas. # Users and teams The Management API models access in layers. Users belong to an organization, teams group users, roles define capabilities, and scopes define which resources those capabilities apply to. Access is granted by giving a team one or more roles under a scope, then adding users to the team. Granting access team-by-team rather than user-by-user keeps it reusable and easy to audit. The usual sequence is: identify the users, inspect the available roles and scopes, create or update a team that maps roles to a scope, and add members. ## List users, roles, and scopes Start by looking at what already exists. List the users in the organization: ```bash curl -X GET "https://app.mk.io/api/v1/organization/users" \ -H "Authorization: Bearer " ``` Inspect the roles and scopes the organization defines, which are the building blocks of a team's access. A role record lists its `capabilities`; a scope record lists the `resources` it covers. ```bash curl -X GET "https://app.mk.io/api/v1/organization/roles" \ -H "Authorization: Bearer " curl -X GET "https://app.mk.io/api/v1/organization/scopes" \ -H "Authorization: Bearer " ``` ## Create a team A team is created or replaced with `PUT`. The `spec` holds `members` (a map keyed by user ID, where `isTeamAdmin` lets a member edit the team) and `scopes` (a map keyed by scope name, each with a `roles` array). ```bash curl -X PUT "https://app.mk.io/api/v1/organization/teams/video-engineering" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "description": "Team for media workflow operations", "members": { "": { "isTeamAdmin": true } }, "scopes": { "": { "roles": [""] } } } }' ``` A `PUT` replaces the whole team spec. To change one thing on an existing team, use JSON Patch instead, as shown next. ## Evolve a team with JSON Patch The team `PATCH` endpoint takes a JSON Patch document (an array of operations), which is the safe way to change one part of a team without resending the whole spec. Add a member: ```bash curl -X PATCH "https://app.mk.io/api/v1/organization/teams/video-engineering" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "op": "add", "path": "/spec/members/", "value": { "isTeamAdmin": false } } ]' ``` Add a role under a scope: ```bash curl -X PATCH "https://app.mk.io/api/v1/organization/teams/video-engineering" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "op": "add", "path": "/spec/scopes//roles/-", "value": "" } ]' ``` Remove a member: ```bash curl -X PATCH "https://app.mk.io/api/v1/organization/teams/video-engineering" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '[ { "op": "remove", "path": "/spec/members/" } ]' ``` The same `op` values (`add`, `replace`, `remove`) work for scopes and roles. Use `replace` on `/spec/members//isTeamAdmin` to promote or demote a member. ## Check what a token can actually do To see the capabilities the current token holds, read its role-based access control (RBAC) data. This is the fastest way to debug a `403 Forbidden`, and it is the starting point for building a restricted token. ```bash curl -X GET "https://app.mk.io/api/v1/user/rbac" \ -H "Authorization: Bearer " ``` ## What goes wrong - **A `PUT` wipes team members you meant to keep.** `PUT` replaces the entire spec. To change one member or role on a live team, use the JSON Patch endpoint. - **Deleting a team does not remove the users.** It removes the team and the access it granted; the users remain in the organization. - **A restricted token cannot exceed your own access.** When you design one, compare it against `/api/v1/user/rbac`; the token's permissions must be a subset. See [Tokens](/api-guides/how-to/management/tokens). ## What comes next - [Organizations and invites](/api-guides/how-to/management/organizations-and-invites): the invitation lifecycle for adding new users. - [Tokens](/api-guides/how-to/management/tokens): create restricted automation tokens against this access model. # Webhook rules This guide covers the management side of webhooks: creating rules, updating them, and reading delivery history. For the event model and the delivered payload shape, see [Webhooks](/api-guides/understanding/webhooks). Each rule is project-scoped and identified by a stable name in the URL. The practical pattern is to name the rule for its purpose, create or update it with `PUT`, read it back to confirm the non-secret configuration, and inspect its `/events` history when debugging delivery. ## Create or update a rule A rule is created or replaced with `PUT` at its name. The `spec` requires `enabled`, `url`, and `events`. Put secrets in `authentication.headers` or `authentication.queryParams`, which are write-only; put non-sensitive decoration in the top-level `headers` and `queryParams`. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//webhook/rules/job-notifications" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "url": "https://your-endpoint.example.com/webhook", "enabled": true, "events": ["MediaKind.JobStarted", "MediaKind.JobFinished"], "authentication": { "headers": { "X-Webhook-Secret": "" } }, "headers": { "X-Source": "mkio" } } }' ``` Because `PUT` replaces the whole rule, include the full `spec` each time. To pause delivery without losing the rule, send the same `PUT` with `enabled` set to `false`. ## Read the rule ```bash curl -X GET "https://app.mk.io/api/v1/projects//webhook/rules" \ -H "Authorization: Bearer " curl -X GET "https://app.mk.io/api/v1/projects//webhook/rules/job-notifications" \ -H "Authorization: Bearer " ``` Authentication values read back masked, so use the read response to confirm the structure, enabled state, URL, events, and non-secret headers, not the secrets themselves. ## Inspect delivery history When an application reports a missing event, the rule's `events` collection is the first place to look: ```bash curl -X GET "https://app.mk.io/api/v1/projects//webhook/rules/job-notifications/events" \ -H "Authorization: Bearer " ``` Each record carries `created`, `source`, `type`, and a `status` of `Pending`, `Retrying`, `Failed`, or `Sent`. A `Failed` or stuck `Retrying` status points at the receiving endpoint rather than MK.IO. ## Delete a rule ```bash curl -X DELETE "https://app.mk.io/api/v1/projects//webhook/rules/job-notifications" \ -H "Authorization: Bearer " ``` Deleting a rule also removes its event history, so export anything you need first. ## What goes wrong - **A `PUT` drops fields you forgot to include.** It replaces the entire `spec`. Read the rule, change what you need, and send the complete spec back. - **Secrets look wrong on read.** Authentication values are write-only and read back masked. That is expected; they are still sent on delivery. - **Events never arrive.** Check the `/events` history. A `Failed` status means your endpoint rejected the delivery, so verify it is reachable over HTTPS and returns a success response. See the handler guidance in [Webhooks](/api-guides/understanding/webhooks). ## What comes next - [Webhooks](/api-guides/understanding/webhooks): the payload model, supported events, and handler design. - [Transforms and jobs](/api-guides/how-to/media/transforms-and-jobs): pair job workflows with completion events. # Build with the Media API The Media API is the operational API for media processing and delivery in MK.IO. Use it when you need to register storage, create assets, run transforms and jobs, manage live events, publish content with streaming locators, or protect playback with streaming and content key policies. ## API overview | Detail | Value | | --- | --- | | Base path | `/api/v1/projects/{project_name}/media/` | | Scope | Project | | Reference | [Media API reference](/api-reference/media-api) | ## How the core resources connect The Media API becomes much easier to use once you understand the sequence: 1. A **storage** instance tells MK.IO how to reach cloud storage. 2. An **asset** points to content in that storage. 3. A **transform** defines processing instructions. 4. A **job** applies the transform to the asset and creates output assets. 5. A **streaming locator** publishes an asset by linking it to a **streaming policy**. 6. A **streaming endpoint** provides the delivery domain for the playback paths returned by `listPaths`. 7. A **content key policy** becomes part of the chain when you need DRM or token-based key delivery. That resource model supports both VOD and live workflows. The difference is whether the asset is created from existing stored content or continuously written by a live output. ## Common workflow patterns ## Guides - [Storage](/api-guides/how-to/media/storage): Register Azure, AWS, or Google storage and manage the credentials MK.IO uses to access it. - [Assets](/api-guides/how-to/media/assets): Create assets, inspect tracks, request file access information, organize content with labels, and apply filters. - [Download asset files](/api-guides/how-to/media/download-asset-files): Retrieve one file from an asset by requesting access information, then using the returned access URL and JWT. - [Playback filters](/api-guides/how-to/media/playback-filters): Create asset or account filters for time ranges, track selection, and startup quality, then apply them through locators. - [Transforms and jobs](/api-guides/how-to/media/transforms-and-jobs): Define reusable processing profiles and run them against assets. - [Live streaming](/api-guides/how-to/media/live-streaming): Create live events, connect encoders, archive with live outputs, and publish live playback. - [Streaming and publishing](/api-guides/how-to/media/publishing): Create streaming endpoints, locators, and policies, then retrieve playback URLs. - [Content protection](/api-guides/how-to/media/content-protection): Add DRM and key-delivery configuration to published assets. ## Workflow guide - [Automate a VOD pipeline](/api-guides/how-to/media/vod-pipeline): Follow an end-to-end flow from source asset through transform job to published playback. # Assets An asset is the core media record in the Media API. It points to content in a registered storage location and becomes the unit you pass into jobs, attach to streaming locators, inspect for tracks, and organize with labels. An asset maps to a container in Azure or a bucket in AWS storage. See [Assets](/mkio/understanding/core-concepts/assets) for the product background. In a typical workflow you create an asset that points to source content or a target output location, run a job against it or write live output into it, then publish it through a streaming locator. ## Create an asset An asset is created with `PUT`. The only required field is `properties.storageAccountName`, which names the storage instance you registered. The rest position the asset within that storage and attach metadata. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/assets/source-video" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "storageAccountName": "primary-azure", "container": "source-video", "description": "Source video for encoding", "subPath": "incoming", "containerDeletionPolicy": "Retain" }, "labels": { "series": "my-show", "season": "2" } }' ``` The fields worth knowing: - `storageAccountName` (required): the registered storage instance to use. - `container`: the storage container or bucket for the asset. - `subPath`: a directory path inside the container. It is immutable after creation. - `containerDeletionPolicy`: `Delete` or `Retain`. It controls whether deleting the asset also deletes the underlying storage container. - `labels`: up to 32 key-value pairs, used for filtering and grouping. Storage placement is fixed at creation. Treat `storageAccountName`, `container`, and `subPath` as create-time decisions in your automation. ## Read the asset or just its state Read the full asset with a `GET`: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets/source-video" \ -H "Authorization: Bearer " ``` When you only need readiness in a polling loop, read the lightweight state endpoint instead of the full object. See [Resource states](/api-guides/understanding/resource-states) for the asset state values. ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets/source-video/state" \ -H "Authorization: Bearer " ``` ## Inspect tracks and request file access Two read operations are commonly useful once an asset exists. Enumerate the container contents and track listings, including language and bitrate where available: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets/source-video/storage/" \ -H "Authorization: Bearer " ``` Request the information needed to read files directly from the underlying storage: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/assets/source-video/getFileAccessInfo" \ -H "Authorization: Bearer " ``` The response returns `storageAccountName`, `containerName`, `jwt`, `url`, and an optional `subPath`. The storage instance must have a valid credential for this to succeed. For the full two-step download flow, see [Download asset files](/api-guides/how-to/media/download-asset-files). ## Organize assets with labels Labels are part of the asset schema and are designed for retrieval, not just description. Query by an exact label value: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$label=series=my-show" \ -H "Authorization: Bearer " ``` Or require the presence of several keys at once: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$label_key=series&\$label_key=region" \ -H "Authorization: Bearer " ``` The asset list also supports `$top`, `$skiptoken`, `$orderby`, and `$filter`. See [Pagination and filtering](/api-guides/understanding/pagination). ## See where an asset is published To find out whether an asset is already exposed for playback, list the locators attached to it rather than scanning every locator in the project: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/assets/source-video/listStreamingLocators" \ -H "Authorization: Bearer " ``` ## What goes wrong - **Deleting an asset can delete the content.** When `containerDeletionPolicy` is `Delete`, removing the asset also removes the underlying container and everything in it. Set `Retain` when the stored files must survive the asset record. - **`getFileAccessInfo` fails after a credential expires.** The most common cause is an expired storage credential. Rotate it first; see [Storage](/api-guides/how-to/media/storage). - **Trying to move an asset after creation.** Storage placement fields are immutable. To relocate content, create a new asset. ```bash curl -X DELETE "https://app.mk.io/api/v1/projects//media/assets/source-video" \ -H "Authorization: Bearer " ``` ## What comes next - [Transforms and jobs](/api-guides/how-to/media/transforms-and-jobs): process assets through reusable transforms. - [Playback filters](/api-guides/how-to/media/playback-filters): publish a shaped subset of an asset. - [Streaming and publishing](/api-guides/how-to/media/publishing): publish assets for playback. # Content protection To configure protected playback with Digital Rights Management (DRM) using the Media API, you combine three resources that meet at the streaming locator: - A **content key policy** defines *how* decryption keys and DRM licenses are delivered, and *who* is allowed to receive them. - A **streaming policy** defines *how* the content is encrypted for playback. - A **streaming locator** ties a policy pair to an asset and publishes it. Separating these resources lets you reuse one key-delivery policy across many assets while choosing a different encryption model for each publication context. You need a content key policy whenever playback must be restricted to entitled viewers or encrypted with DRM, and you can skip it entirely for unprotected playback. This guide builds a working multi-DRM policy. For conceptual background on each DRM system, see the [content protection overview](/mkio/understanding/core-concepts/content-protection). ## How the pieces fit together The protected playback chain is always the same three steps: 1. Create or choose a **content key policy** (the subject of this guide). 2. Create or choose a **streaming policy** (predefined policies cover most cases). 3. Create a **streaming locator** that references the asset, the streaming policy, and the content key policy. The rest of this guide builds that chain. ## The minimal working policy A content key policy is a named resource. Its `properties.options` array holds one or more *options*, and every option pairs two things: - a `configuration`: the DRM system and its license rules. - a `restriction`: who is allowed to receive a license. The most common production setup is multi-DRM (PlayReady and Widevine) gated behind a JSON Web Token (JWT). The request below creates that policy. Replace `` and the verification key with your own values. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/contentKeyPolicies/multi-drm-policy" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "description": "PlayReady and Widevine, gated by a JWT token", "options": [ { "name": "playready-jwt", "configuration": { "@odata.type": "#Microsoft.Media.ContentKeyPolicyPlayReadyConfiguration", "licenses": [ { "allowTestDevices": false, "licenseType": "NonPersistent", "contentType": "Unspecified", "contentKeyLocation": { "@odata.type": "#Microsoft.Media.ContentKeyPolicyPlayReadyContentEncryptionKeyFromHeader" } } ] }, "restriction": { "@odata.type": "#Microsoft.Media.ContentKeyPolicyTokenRestriction", "issuer": "https://your-company.com", "audience": "your-video-app", "restrictionTokenType": "Jwt", "primaryVerificationKey": { "@odata.type": "#Microsoft.Media.ContentKeyPolicySymmetricTokenKey", "keyValue": "" } } }, { "name": "widevine-jwt", "configuration": { "@odata.type": "#Microsoft.Media.ContentKeyPolicyWidevineConfiguration", "widevineTemplate": "{}" }, "restriction": { "@odata.type": "#Microsoft.Media.ContentKeyPolicyTokenRestriction", "issuer": "https://your-company.com", "audience": "your-video-app", "restrictionTokenType": "Jwt", "primaryVerificationKey": { "@odata.type": "#Microsoft.Media.ContentKeyPolicySymmetricTokenKey", "keyValue": "" } } } ] } }' ``` The fields that matter most: - `@odata.type` is the discriminator on every nested object. It selects which configuration, restriction, or key type the object is. The values are fixed strings and must match exactly. - `widevineTemplate` is a JSON string, not a JSON object. An empty `"{}"` tells MK.IO to generate a default Widevine license. Supply a custom template only when you need to control persistence or validity. - `keyValue` is the Base64-encoded HMAC secret your application signs tokens with. It is the same secret on both sides: the policy verifies what your token server signs. - `issuer` and `audience` must match the `iss` and `aud` claims your tokens carry. A mismatch rejects the license request. A successful create returns `201 Created` with the stored policy. A name that already exists returns `409 Conflict`. For the full request and response schema, see the [Media API reference](/api-reference/media-api). ## Choosing the configuration The `configuration` block selects the DRM system. Each system covers different platforms, so the choice is driven by the devices you need to reach. | You need to reach | Use this configuration | `@odata.type` | | :--- | :--- | :--- | | Edge, Xbox, Windows | PlayReady | `#Microsoft.Media.ContentKeyPolicyPlayReadyConfiguration` | | Chrome, Firefox, Android, Android TV | Widevine | `#Microsoft.Media.ContentKeyPolicyWidevineConfiguration` | | Safari, iOS, tvOS, macOS | FairPlay | `#Microsoft.Media.ContentKeyPolicyFairPlayConfiguration` | | Basic encryption, lowest latency, no DRM license server | Clear Key | `#Microsoft.Media.ContentKeyPolicyClearKeyConfiguration` | To cover every platform, include all three DRM configurations in one policy, as the minimal example does for two of them. FairPlay is the one configuration that needs extra setup: it requires a certificate (`fairPlayPfx`), its password (`fairPlayPfxPassword`), and an application secret key (`ask`), all Base64-encoded. The other systems work with the fields shown above. Clear Key uses AES-128 and does not reach the security of the three DRM systems. Do not add Clear Key to a policy intended for DRM, because it lowers the overall protection. For the per-system setup detail, see [multi-DRM encryption](/mkio/how-to/drm-content-protection/multi-drm-encryption). ## Choosing the restriction The `restriction` block decides who receives a license. | Your situation | Use this restriction | `@odata.type` | | :--- | :--- | :--- | | Production: only entitled viewers may play | Token restriction | `#Microsoft.Media.ContentKeyPolicyTokenRestriction` | | Testing only: anyone with the stream may play | Open restriction | `#Microsoft.Media.ContentKeyPolicyOpenRestriction` | An open restriction needs no other fields and is for testing only. A token restriction is the production default and needs `issuer`, `audience`, `restrictionTokenType`, and a `primaryVerificationKey`. The verification key type is itself chosen by `@odata.type`: | Signing approach | Key type | `@odata.type` | Required fields | | :--- | :--- | :--- | :--- | | Symmetric (HMAC, HS256) | Symmetric | `#Microsoft.Media.ContentKeyPolicySymmetricTokenKey` | `keyValue` | | Asymmetric (RSA) | RSA | `#Microsoft.Media.ContentKeyPolicyRsaTokenKey` | `modulus`, `exponent` | | Certificate (X.509) | X.509 | `#Microsoft.Media.ContentKeyPolicyX509CertificateTokenKey` | `rawBody` | Symmetric (HS256) is the default and the simplest. Choose RSA when your security model requires asymmetric keys; see [RSA key for token validation](/mkio/how-to/drm-content-protection/rsa-key-for-token-validation). ## Attach the policy at the locator A content key policy takes effect only when a streaming locator references it. The locator joins the asset, a streaming policy, and the content key policy. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/streamingLocators/protected-locator" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "assetName": "output-video", "streamingPolicyName": "Predefined_MultiDrmCencStreaming", "defaultContentKeyPolicyName": "multi-drm-policy" } }' ``` Use a predefined streaming policy unless you have a reason not to. The predefined policies remove all custom encryption configuration: | Streaming policy | Encryption | Requires in the content key policy | | :--- | :--- | :--- | | `Predefined_ClearStreamingOnly` | None | Nothing | | `Predefined_ClearKey` | Clear Key (AES-128) | A Clear Key option | | `Predefined_MultiDrmCencStreaming` | PlayReady and Widevine | Both PlayReady and Widevine options | | `Predefined_MultiDrmStreaming` | PlayReady, Widevine, and FairPlay | All three options | The minimal example pairs with `Predefined_MultiDrmCencStreaming` because it defines PlayReady and Widevine. To add Safari and Apple devices, add a FairPlay option to the policy and switch the locator to `Predefined_MultiDrmStreaming`. ## What goes wrong - **The streaming policy and the content key policy disagree.** The streaming policy enforces which DRM schemes the content key policy must contain. A `Predefined_MultiDrmStreaming` locator fails if the policy does not include all three schemes, and a `Predefined_MultiDrmCencStreaming` locator fails if it lacks PlayReady or Widevine. Match the table above. - **The token is signed with a different key, issuer, or audience.** MK.IO validates the token signature against `primaryVerificationKey`, and the `iss` and `aud` claims against the policy. Any mismatch rejects the license, even when the stream itself publishes correctly. - **The verification key leaks into client code.** Generate tokens on your server and pass them to the player at runtime. Never ship the `keyValue` secret in client-side code. - **Playback shows a license error during testing.** A protected stream opened without a token can surface a `DRM_FAILED_LICENSE_REQUEST` error. That is expected: the encrypted stream needs a token that has not been provided yet. For the token flow and the required claims, see [JWT token authentication](/mkio/how-to/drm-content-protection/jwt-token-authentication). ## Related operations List the content key policies in a project: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/contentKeyPolicies" \ -H "Authorization: Bearer " ``` Retrieve a single policy: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/contentKeyPolicies/multi-drm-policy" \ -H "Authorization: Bearer " ``` A standard `GET` never returns secret values, such as the verification key. When you need to inspect or rotate secrets, use the secrets variant, and only then: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/contentKeyPolicies/multi-drm-policy/getPolicyPropertiesWithSecrets" \ -H "Authorization: Bearer " ``` ## Where to go deeper For the concepts and platform-level setup behind these requests: - [Content protection overview](/mkio/understanding/core-concepts/content-protection): how DRM, keys, and licenses work in MK.IO. - [Multi-DRM encryption](/mkio/how-to/drm-content-protection/multi-drm-encryption): per-system configuration detail. - [JWT token authentication](/mkio/how-to/drm-content-protection/jwt-token-authentication): the token flow and claim requirements. - [Using custom claims in content key policies](/mkio/how-to/drm-content-protection/custom-claims): route different licenses from one policy. For the complete, field-level request and response schema, including every configuration and restriction variant this guide does not cover: - [Media API reference](/api-reference/media-api). ## What comes next - [Streaming and publishing](/api-guides/how-to/media/publishing): connect the policy to locators and live playback. # Download asset files MK.IO does not proxy file transfers. To read a file from an asset, you call `getFileAccessInfo` to get access details for the asset's underlying cloud storage, then talk to Azure Blob Storage or AWS S3 directly. Uploads work the same way: you write to your own storage, because there is no upload endpoint on the asset. You need an asset that already points to stored content, a bearer token, the asset name, and the path of the file you want. ## Get the access details `getFileAccessInfo` is a `POST` with no body: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/assets//getFileAccessInfo" \ -H "Authorization: Bearer " ``` The response carries everything needed to reach the storage directly: ```json { "storageAccountName": "mystorageaccount", "containerName": "mycontainer", "jwt": "", "url": "", "subPath": "incoming" } ``` | Field | Description | | :--- | :--- | | `url` | Base URL of the storage container. | | `jwt` | Token to authenticate the follow-up request. | | `storageAccountName` | The registered storage account or S3 bucket name. | | `containerName` | The container or bucket holding the asset files. | | `subPath` | Optional path prefix. Include it when building the file path. | ## Build the file path Combine `subPath`, if present, with the file name. If `subPath` is `incoming` and the file is `manifest.mpd`, the path is `incoming/manifest.mpd`. With no `subPath`, the path is just `manifest.mpd`. ## Download the file Use the returned `url` as the base, append the file path, and pass the `jwt` as the bearer token: ```bash curl -X GET "//" \ -H "Authorization: Bearer " \ --output ``` This follows the standard [Azure Blob Storage Get Blob](https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob) pattern for Azure-backed assets. For S3-backed assets, inspect the returned `url`. If it is a presigned URL (it contains query parameters such as `X-Amz-Signature`), the auth is embedded and you do not pass a separate token: ```bash curl -X GET "" --output ``` If it is a plain bucket URL, pass the `jwt` as the bearer token as in the Azure example. ## Upload files There is no MK.IO endpoint for uploading into an asset container, and `getFileAccessInfo` returns read-scoped access intended for download. To upload, write directly to your cloud storage with the credentials on your registered storage instance: - **Azure**: use the [Put Blob REST API](https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob) or the Azure Storage SDK with the SAS token configured on the storage instance. - **AWS S3**: use the [PutObject API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) or the AWS SDK with the credentials configured on the storage instance. ## What goes wrong - **`getFileAccessInfo` fails.** The most common cause is an expired storage credential. Rotate it; see [Storage](/api-guides/how-to/media/storage). - **Using the access token to upload.** The returned access is read-scoped. Use your own storage credentials for writes. ## What comes next - [Assets](/api-guides/how-to/media/assets): manage the assets these files belong to. - [Storage](/api-guides/how-to/media/storage): rotate credentials if access stops working. # Live streaming A live workflow in the Media API is built from a few resources that you manage independently: a **live event** defines ingest and encoding, a **live output** archives the running stream into an asset, a **streaming locator** publishes that asset, and a **streaming endpoint** serves the playback hostname. Separating ingest, archive, and playback lets you, for example, keep an archive asset for VOD long after the event ends. See [Live events](/mkio/understanding/core-concepts/live-event) for the product background. ## Create the live event A live event is created with `PUT`. Several of its properties are create-time only, including `input`, `encoding`, `streamOptions`, and `useStaticHostname`, so create it only when you are ready to use it. The example below is a passthrough event that ingests over RTMP. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/liveEvents/main-stage" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "tags": {}, "properties": { "useStaticHostname": false, "streamOptions": ["Default"], "encoding": { "encodingType": "PassthroughBasic" }, "input": { "streamingProtocol": "RTMP", "accessControl": { "ip": { "allow": [] } }, "accessToken": "", "keyFrameIntervalDuration": "PT2S", "timedMetadataEndpoints": [] } } }' ``` An empty `accessControl.ip.allow` array allows ingest from any address. For production, list the encoder addresses explicitly. When `useStaticHostname` is `true` you can also set `hostnamePrefix` for a stable ingest and preview hostname; when it is `false`, MK.IO generates the addresses. ## Choosing an encoding type `encodingType` decides whether MK.IO forwards your encoder's output as-is or transcodes it into an adaptive ladder. Passthrough is cheaper and lower-latency but pushes the adaptive-bitrate work onto your encoder; the encoding types produce the ladder for you. | `encodingType` | What it does | Ingest cap | Maximum archive | | :--- | :--- | :--- | :--- | | `PassthroughBasic` | Forwards incoming layers, no transcoding | 5 Mbps | 8 hours | | `PassthroughStandard` | Forwards incoming layers, no transcoding | 60 Mbps | 25 hours | | `Standard` | Transcodes to a 720p adaptive ladder | - | 25 hours | | `Premium1080p` | Transcodes to a 1080p adaptive ladder | - | 25 hours | For ingest protocol, `RTMP` and `RTMPS` work with passthrough and encoding. `SRT` (Secure Reliable Transport) is supported for the encoding types. ## Start the event and get the ingest URL Start the event when you are ready to receive the stream: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/liveEvents/main-stage/start" \ -H "Authorization: Bearer " ``` Then read the event back. The `input.endpoints` field is populated server-side and holds the ingest URLs you give to your encoder: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/liveEvents/main-stage" \ -H "Authorization: Bearer " ``` Point your encoder at that URL. Configure the encoder's keyframe interval (Group of Pictures, or GOP) to match `keyFrameIntervalDuration`. For example, at 30 frames per second with a 2-second interval, set the encoder to a 60-frame GOP. ## Archive the stream with a live output A live output writes the running stream into an asset. It requires `assetName` and `archiveWindowLength`, an ISO 8601 duration that sets the Digital Video Recorder (DVR) window. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/liveEvents/main-stage/liveOutputs/main-archive" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "assetName": "main-stage-archive", "archiveWindowLength": "PT4H", "manifestName": "index", "description": "Archive of the main stage feed" } }' ``` ## Publish live playback The live output writes into an asset, so you publish it the same way as VOD: create a streaming locator on the asset, then retrieve its paths. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/streamingLocators/main-stage-live" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "assetName": "main-stage-archive", "streamingPolicyName": "Predefined_ClearStreamingOnly" } }' ``` Combine the paths from `listPaths` with the `hostName` of a running streaming endpoint to form the playback URLs. See [Streaming and publishing](/api-guides/how-to/media/publishing) for the full publishing model. ## Stop, reset, and delete Stopping is a deliberate, billing-relevant action. Stop the event when the stream ends: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/liveEvents/main-stage/stop" \ -H "Authorization: Bearer " ``` `reset` restarts ingest on the same event without recreating it. To tear the event down, delete the live output first, then the event: ```bash curl -X DELETE "https://app.mk.io/api/v1/projects//media/liveEvents/main-stage/liveOutputs/main-archive" \ -H "Authorization: Bearer " curl -X DELETE "https://app.mk.io/api/v1/projects//media/liveEvents/main-stage" \ -H "Authorization: Bearer " ``` The archived asset survives the event and can keep serving VOD playback. ## What goes wrong - **Ingest stops after a pause.** RTMP and RTMPS drop the connection after roughly 10 seconds with no data. Encoder pauses between scenes, network instability, or an idle source can all trigger this. Keep the feed flowing. - **Playback stutters or fails because keyframes do not align.** The encoder GOP must match `keyFrameIntervalDuration`. A mismatch produces broken segments. - **Ongoing charges after the broadcast.** A running live event and a running streaming endpoint both bill while active. Stop both when the broadcast ends, and delete assets you no longer need. - **Trying to allocate before starting.** The `allocate` operation is not implemented and returns an error. Start the event directly. - **Editing a create-time field.** Input, encoding, stream options, and static-hostname settings cannot change after creation. To change them, create a new event. ## What comes next - [Streaming and publishing](/api-guides/how-to/media/publishing): locators, policies, endpoints, and playback URLs. - [Webhooks](/api-guides/understanding/webhooks): react to `MediaKind.ChannelInstanceStarted`, `ChannelInstanceStopped`, and `ChannelInstanceError` without polling. # Playback filters A filter publishes a shaped version of an asset without creating a new one. Use a filter when the manifest should expose only part of the content, only certain tracks, or a different startup quality. Filters shape the manifest at playback time; they do not reprocess content. The asset holds the full media, the filter describes the subset, and the streaming locator applies the filter when it publishes. ## Choosing the filter scope There are two scopes, and the choice is about reuse. | Scope | Path | Use when | | :--- | :--- | :--- | | Asset filter | `.../media/assets/{asset_name}/assetFilters/{filter_name}` | The rule belongs to one title or archive. | | Account filter | `.../media/accountFilters/{filter_name}` | The same rule should apply across many assets in the project. | Both use the same `MediaFilterProperties` body, which supports three controls: `presentationTimeRange` to clip a time window, `tracks` to select tracks, and `firstQuality` to set the startup bitrate. ## Clip a time window This asset filter exposes only the section from 10 to 70 seconds. With `timescale` set to `1`, the timestamps are in seconds. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/assets/source-video/assetFilters/highlights" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "presentationTimeRange": { "timescale": 1, "startTimestamp": 10, "endTimestamp": 70 } } }' ``` `timescale` is the number of units per second. It defaults to `10000000` (100-nanosecond units), so set it to `1` when you want to work in whole seconds. The same block also supports the live-only fields `presentationWindowDuration` (the rewind window) and `liveBackoffDuration` (the delay from the live edge). ## Select tracks This account filter includes audio tracks that are not English, and video tracks between 3 and 5 megabits per second. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/accountFilters/non-english-mid-bitrate" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "tracks": [ { "trackSelections": [ { "property": "Type", "operation": "Equal", "value": "audio" }, { "property": "Language", "operation": "NotEqual", "value": "en" } ] }, { "trackSelections": [ { "property": "Type", "operation": "Equal", "value": "video" }, { "property": "Bitrate", "operation": "Equal", "value": "3000000-5000000" } ] } ] } }' ``` The selectable properties are `Type` (`video`, `audio`, `text`), `Name`, `Language` (an RFC 5646 tag such as `en` or `en-US`), `FourCC` (a codec such as `avc1` or `mp4a`), and `Bitrate` (a single value or a range like `3000000-5000000`). Values are case-insensitive, and each property uses an `operation` of `Equal` or `NotEqual`. The nesting controls the logic, and it is easy to misread: - Conditions inside one `trackSelections` array are combined with **AND**. - Entries in the top-level `tracks` array are combined with **OR**. So the filter above means "(audio AND not English) OR (video AND 3-5 Mbps)". ## Set the startup quality Add `firstQuality` to start HLS playback near a target bitrate. The closest available rung in the ladder is used if the exact bitrate is absent. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/accountFilters/high-start-quality" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "firstQuality": { "bitrate": 5000000 } } }' ``` ## Apply the filter A filter does nothing until a streaming locator references it by name in the `filters` list. The list accepts both asset and account filters. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/streamingLocators/highlights-locator" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "assetName": "source-video", "streamingPolicyName": "Predefined_ClearStreamingOnly", "filters": ["highlights"] } }' ``` ## What goes wrong - **The filter has no effect.** A filter only applies when a locator lists it in `filters`. Creating the filter alone changes nothing. - **Time values look wrong.** Without `timescale: 1`, timestamps are in 100-nanosecond units by default. Set `timescale` to match the unit you intend. - **A delete is rejected.** A filter cannot be deleted while an active streaming locator still references it. Remove it from the locator first. ```bash curl -X DELETE "https://app.mk.io/api/v1/projects//media/assets/source-video/assetFilters/highlights" \ -H "Authorization: Bearer " ``` ## What comes next - [Streaming and publishing](/api-guides/how-to/media/publishing): apply filters at publication time. - [Assets](/api-guides/how-to/media/assets): use an asset filter when the rule belongs to one asset. # Streaming and publishing Publishing has two parts that meet at the playback URL. A **streaming locator** publishes a specific asset and decides its playback and protection behaviour through a streaming policy. A **streaming endpoint** provides the hostname that players connect to and does the just-in-time packaging and encryption. You need both: a locator gives you a path, an endpoint gives you a host, and the playback URL is the two combined. See [Streaming locators](/mkio/understanding/core-concepts/locators) and [Streaming endpoints](/mkio/understanding/core-concepts/endpoints) for the product background. ## Create and start a streaming endpoint An endpoint is created with `PUT` and requires `location` and `properties.scaleUnits`. A `scaleUnits` of `0` provisions a shared Standard endpoint; a value above `0` provisions dedicated Premium capacity. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/streamingEndpoints/default" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "location": "", "properties": { "scaleUnits": 0, "description": "Default streaming endpoint" } }' ``` Playback only works while the endpoint is running, so start it and confirm the state: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/streamingEndpoints/default/start" \ -H "Authorization: Bearer " curl -X GET "https://app.mk.io/api/v1/projects//media/streamingEndpoints/default/state" \ -H "Authorization: Bearer " ``` The endpoint's `hostName` is the value you combine with locator paths to build playback URLs. To enable a Content Delivery Network (CDN), set `cdnEnabled` to `true`; the response then includes a `cdnBasePath` that goes into the URL after the hostname. ## Publish an asset with a streaming locator A locator is created with `PUT` and requires `assetName` and `streamingPolicyName`. The example publishes an asset for clear (unprotected) streaming. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/streamingLocators/my-locator" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "assetName": "output-video", "streamingPolicyName": "Predefined_ClearStreamingOnly" } }' ``` The locator is also where playback windowing, filters, and protection meet, through optional fields: `startTime` and `endTime` to bound availability, `filters` to apply [playback filters](/api-guides/how-to/media/playback-filters), `defaultContentKeyPolicyName` for [content protection](/api-guides/how-to/media/content-protection), and `suppressed` to take output offline without deleting the locator. ## Choosing a streaming policy The `streamingPolicyName` decides what playback the locator allows. Use a predefined policy unless you need behaviour they do not cover. | Policy | Playback | | :--- | :--- | | `Predefined_DownloadOnly` | Download only, no streaming. | | `Predefined_ClearStreamingOnly` | Streaming, no protection. | | `Predefined_DownloadAndClearStreaming` | Download and clear streaming. | | `Predefined_ClearKey` | Clear Key (AES-128) protection. | | `Predefined_MultiDrmCencStreaming` | PlayReady and Widevine. | | `Predefined_MultiDrmStreaming` | PlayReady, Widevine, and FairPlay. | List the policies available in a project, including any custom ones, with `GET .../media/streamingPolicies`. The DRM policies require a matching content key policy on the locator; see [Content protection](/api-guides/how-to/media/content-protection). ## Build the playback URL Ask the locator for its relative paths with `listPaths`: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/streamingLocators/my-locator/listPaths" \ -H "Authorization: Bearer " ``` The response groups paths by protocol and encryption under `streamingPaths`, and lists any `downloadPaths`. A playback URL is the endpoint hostname (plus `cdnBasePath` if CDN is enabled) followed by a path: ```text https:// ``` The manifest path takes format and encryption options in parentheses. For example, an HLS manifest with Apple sample encryption and two filters applied: ```text https:////manifest.ism/manifest(format=m3u8-cmaf,encryption=cbcs-aapl,filter=account-filter;asset-filter) ``` Use `format=m3u8-cmaf` for HLS and `format=mpd-time-cmaf` for DASH. The MK.IO product docs recommend fetching playback URLs dynamically through `listPaths` rather than hard-coding them, so that origins can change without breaking clients. ## What goes wrong - **Playback returns nothing because the endpoint is stopped.** The endpoint must be in the `Running` state. Start it and confirm before testing. - **A locator returns `404` or `410`.** Before its `startTime`, a locator returns `404 Not Found`; after its `endTime`, it returns `410 Gone`. A `suppressed` locator also returns `404`. Check the time window if playback is unexpectedly unavailable. - **A DRM policy without a key policy.** Selecting `Predefined_MultiDrmCencStreaming` or similar without a matching content key policy on the locator produces unplayable, encrypted output. See [Content protection](/api-guides/how-to/media/content-protection). ## What comes next - [Content protection](/api-guides/how-to/media/content-protection): add DRM and key delivery. - [Automate a VOD pipeline](/api-guides/how-to/media/vod-pipeline): see publishing in an end-to-end workflow. # Storage Most Media API workflows begin with storage. Before MK.IO can process or publish content, it needs a storage instance: its record of an external Azure, AWS, or Google location, together with the credentials it uses to reach that location. The underlying bucket or account stays in your cloud provider. MK.IO only holds the connection details. For the product background on how storage relates to assets, see [Assets](/mkio/understanding/core-concepts/assets). ## Register a storage instance A storage instance is created with `PUT`, and the `spec` body is discriminated by a `type` field. The three supported types are `Microsoft.Storage` (Azure), `AWS.S3`, and `Google.Storage`. The example below registers an Azure account. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/storage/primary-azure" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "type": "Microsoft.Storage", "url": "https://mystorageaccount.blob.core.windows.net", "description": "Input media files for processing.", "credential": { "sasToken": "?sv=2022-11-02&ss=bfqt&srt=co&sp=rwdlacupiyx&se=2026-12-31T23:59:59Z&st=2026-01-01T00:00:00Z&spr=https&sig=" } } }' ``` A few fields are immutable after creation: the Azure `url`, and the `bucketName` for AWS and Google. The shared secret material differs by provider, as the next section shows. ## Choosing the credential for your provider Each provider has a different credential shape and a different immutable identifier. | Provider | `type` | Identifier (immutable) | Credential field | | :--- | :--- | :--- | :--- | | Azure | `Microsoft.Storage` | `url` | `credential.sasToken` (include the leading `?`) | | AWS S3 | `AWS.S3` | `bucketName` | `credential.accessKeyId` and `credential.secretAccessKey` | | Google | `Google.Storage` | `bucketName` | `credential.gac` (the service-account JSON) | An AWS registration looks like this: ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/storage/primary-s3" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "type": "AWS.S3", "bucketName": "my-media-bucket", "description": "Input media files for processing.", "credential": { "accessKeyId": "", "secretAccessKey": "" } } }' ``` ## Rotate a credential before it expires Credentials are immutable once created, so rotation is always a create-new, then delete-old sequence. This matters most for Azure, where an expired Shared Access Signature (SAS) token silently stops asset access. A storage instance can hold many credentials at once, and MK.IO uses the one with the longest remaining expiry, so adding the replacement first means there is no gap. 1. Add the replacement credential to the storage instance: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/storage/primary-azure/credentials" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "spec": { "type": "Microsoft.Storage", "credential": { "sasToken": "?sv=2022-11-02&ss=bfqt&srt=co&sp=rwdlacupiyx&se=2027-12-31T23:59:59Z&st=2027-01-01T00:00:00Z&spr=https&sig=" } } }' ``` 2. Confirm access still works by running an operation that depends on the credential, such as requesting file access on an asset: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/assets/source-video/getFileAccessInfo" \ -H "Authorization: Bearer " ``` 3. Delete the old credential once the new one is working: ```bash curl -X DELETE "https://app.mk.io/api/v1/projects//media/storage/primary-azure/credentials/" \ -H "Authorization: Bearer " ``` ## Update behaviour `PUT` and `PATCH` differ in what they can change: - `PATCH` updates only `description` and `privateLinkServiceConnection`. - `PUT` updates `description`, the credential, and `privateLinkServiceConnection`. To move an Azure instance off private-link access, rotate the SAS token and clear the private-link reference in one `PUT` by setting `privateLinkServiceConnection` to `null`. Disable the private-link setup in Azure as well, then confirm asset access before you treat the change as complete. ## What goes wrong - **An expired SAS token breaks asset access without an obvious error.** Assets in the affected storage stop resolving. Rotate the credential using the create-new, delete-old sequence above. - **A trailing slash on the Azure `url`.** Register the account URL without a trailing `/`. - **Deleting a storage instance that assets still use.** Deletion removes only the MK.IO record, not the underlying account or bucket, but assets that depend on it stop resolving. Check for dependent assets first: ```bash curl -X DELETE "https://app.mk.io/api/v1/projects//media/storage/primary-azure" \ -H "Authorization: Bearer " ``` ## What comes next - [Assets](/api-guides/how-to/media/assets): create assets that point to content in this storage. - [Automate a VOD pipeline](/api-guides/how-to/media/vod-pipeline): see how storage feeds the rest of a workflow. # Transforms and jobs A transform describes what processing should happen. A job applies that processing to a specific input. Together they are the core of Video on Demand (VOD) processing in the Media API. A transform is a recipe you write once; a job runs that recipe against an asset and writes one or more output assets. You usually create a small number of transforms and reuse them across many jobs. ## Create a transform A transform is created with `PUT`. Its `outputs` array holds the preset that defines the processing. The example below builds an adaptive-bitrate encoding transform using a built-in encoder preset. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/transforms/abr-720p" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "description": "H.264 720p multi-bitrate encoding", "outputs": [ { "preset": { "@odata.type": "#Microsoft.Media.BuiltInStandardEncoderPreset", "presetName": "H264MultipleBitrate720p" }, "onError": "StopProcessingJob", "relativePriority": "Normal" } ] } }' ``` Each output also accepts `onError` (`ContinueJob` or `StopProcessingJob`) and `relativePriority` (`High`, `Normal`, or `Low`). ## Choosing a preset The preset is selected by `@odata.type`. The most common choice is the built-in encoder, but the Media API also offers format conversion, thumbnails, track insertion, and an Artificial Intelligence (AI) pipeline. | To... | Use preset `@odata.type` | Key field | | :--- | :--- | :--- | | Encode to adaptive bitrate H.264 or H.265 | `#Microsoft.Media.BuiltInStandardEncoderPreset` | `presetName` | | Convert or repackage to MP4 without re-encoding | `#Microsoft.Media.BuiltInAssetConverterPreset` | `presetName` | | Generate thumbnails | `#MediaKind.ThumbnailGeneratorPreset` | `thumbnails` | | Insert a track (for example, captions) | `#MediaKind.TrackInserterPreset` | `tracks` | | Run an AI pipeline (transcription, translation) | `#MediaKind.AIPipelinePreset` | `pipeline` | For the built-in encoder, `presetName` chooses the encoding ladder. Single-bitrate options include `H264SingleBitrate720p`, `H264SingleBitrate1080p`, `H265SingleBitrate1080p`, and `H265SingleBitrate4K`. Multi-bitrate options include `H264MultipleBitrateSD`, `H264MultipleBitrate720p`, and `H264MultipleBitrate1080p`, each also available in a `WithCVQ` variant. The [Media API reference](/api-reference/media-api) lists the complete enum. ## Run a job A job is created with `PUT` under the transform. It needs an `input` and `outputs`. The input is discriminated by `@odata.type`: use `JobInputAsset` to process a stored asset, or `JobInputHttp` to pull from an external HTTP source. Outputs are always `JobOutputAsset`. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/transforms/abr-720p/jobs/job-001" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "input": { "@odata.type": "#Microsoft.Media.JobInputAsset", "assetName": "input-video", "files": ["video.mp4"] }, "outputs": [ { "@odata.type": "#Microsoft.Media.JobOutputAsset", "assetName": "output-video" } ], "description": "Encode input-video with the 720p transform", "priority": "Normal" } }' ``` The output asset named here is created and populated by the job. It does not need to exist beforehand. ## Clip the input `JobInputAsset` accepts `start` and `end` clip times. Use `AbsoluteClipTime` with an ISO 8601 duration to process only part of the source: ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/transforms/abr-720p/jobs/clip-job" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "input": { "@odata.type": "#Microsoft.Media.JobInputAsset", "assetName": "input-video", "start": { "@odata.type": "#Microsoft.Media.AbsoluteClipTime", "time": "PT0S" }, "end": { "@odata.type": "#Microsoft.Media.AbsoluteClipTime", "time": "PT30S" } }, "outputs": [ { "@odata.type": "#Microsoft.Media.JobOutputAsset", "assetName": "clip-output" } ] } }' ``` ## Monitor the job Poll the job state endpoint while you wait: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/transforms/abr-720p/jobs/job-001/state" \ -H "Authorization: Bearer " ``` A job moves through these states: | State | Meaning | | :--- | :--- | | `Queued` | Waiting to be processed. | | `Scheduled` | Accepted and scheduled for work. | | `Processing` | Running. | | `Finished` | Completed successfully. | | `Error` | Completed with an error. | | `Canceling` | Cancellation is in progress. | | `Canceled` | Cancelled. | For background workers and high-volume pipelines, subscribe to the `MediaKind.JobStarted` and `MediaKind.JobFinished` [webhooks](/api-guides/understanding/webhooks) instead of polling. ## Manage a running job Raise the priority of a queued job: ```bash curl -X PATCH "https://app.mk.io/api/v1/projects//media/transforms/abr-720p/jobs/job-001" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "priority": "High" } }' ``` Cancel a job that is no longer needed: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/transforms/abr-720p/jobs/job-001/cancelJob" \ -H "Authorization: Bearer " ``` ## What goes wrong - **An invalid `presetName`.** `presetName` must be one of the documented enum values, such as `H264MultipleBitrate720p`. A made-up name like `AdaptiveStreaming` is rejected. Check the [Media API reference](/api-reference/media-api) for the exact list. - **Publishing the input asset instead of the output.** A job writes to the output asset. Publish that one, not the source. - **A job reaches `Error`.** The job output object carries an `error` field with the detail. Read the full job, not just `/state`, to see it. ## What comes next - [Automate a VOD pipeline](/api-guides/how-to/media/vod-pipeline): transforms and jobs in a full publishing workflow. - [Streaming and publishing](/api-guides/how-to/media/publishing): publish the job output for playback. - [Webhooks](/api-guides/understanding/webhooks): replace status polling with events. # Automate a VOD pipeline This guide walks the complete Video on Demand (VOD) path through the Media API, from a source asset to a playable URL. The individual resources have their own guides; the goal here is to show how they connect so you can build a pipeline with less trial and error. The sequence is: 1. Create an input asset that points to the source content. 2. Create or reuse a transform. 3. Create a job that applies the transform to the input asset. 4. Wait for the job to finish and the output asset to be ready. 5. Create a streaming locator on the output asset. 6. Start a streaming endpoint and call `listPaths` for the playback URLs. You need a project with storage already configured, a personal API token, and source content in that storage. ## Step 1: Create the input asset ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/assets/input-video-001" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "storageAccountName": "primary-azure", "container": "input-video-001", "description": "Source video for transcoding" } }' ``` This asset is the input reference for the job. See [Assets](/api-guides/how-to/media/assets) for the full set of fields. ## Step 2: Create or reuse the transform Because transforms are reusable, you usually do this once per processing profile, not once per asset. ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/transforms/standard-encoding" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "description": "H.264 1080p multi-bitrate encoding", "outputs": [ { "preset": { "@odata.type": "#Microsoft.Media.BuiltInStandardEncoderPreset", "presetName": "H264MultipleBitrate1080p" }, "relativePriority": "Normal" } ] } }' ``` `presetName` must be one of the documented enum values. See [Transforms and jobs](/api-guides/how-to/media/transforms-and-jobs) for the available presets. ## Step 3: Create the job ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/transforms/standard-encoding/jobs/job-001" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "input": { "@odata.type": "#Microsoft.Media.JobInputAsset", "assetName": "input-video-001" }, "outputs": [ { "@odata.type": "#Microsoft.Media.JobOutputAsset", "assetName": "output-video-001" } ], "description": "Transcode input-video-001 with the standard transform" } }' ``` The job creates and populates the output asset named here. ## Step 4: Wait for the job to finish Poll the job state until it reaches `Finished`: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/transforms/standard-encoding/jobs/job-001/state" \ -H "Authorization: Bearer " ``` For background automation, subscribe to the `MediaKind.JobStarted` and `MediaKind.JobFinished` [webhooks](/api-guides/understanding/webhooks) instead of polling. ## Step 5: Publish the output asset Create a streaming locator on the output asset, not the source: ```bash curl -X PUT "https://app.mk.io/api/v1/projects//media/streamingLocators/locator-001" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "properties": { "assetName": "output-video-001", "streamingPolicyName": "Predefined_ClearStreamingOnly" } }' ``` ## Step 6: Start the endpoint and get the URLs Start the streaming endpoint if it is not already running, then list the locator paths: ```bash curl -X POST "https://app.mk.io/api/v1/projects//media/streamingEndpoints/default/start" \ -H "Authorization: Bearer " curl -X POST "https://app.mk.io/api/v1/projects//media/streamingLocators/locator-001/listPaths" \ -H "Authorization: Bearer " ``` Combine each path with the endpoint `hostName` to build the playback URLs. See [Streaming and publishing](/api-guides/how-to/media/publishing) for URL construction. ## What goes wrong - **Publishing too early.** A locator can only publish the asset you point it at. In a VOD pipeline, publish the output asset created by the job, after it reaches `Finished`, not the source asset. - **Confusing the endpoint and the locator.** The endpoint provides the delivery hostname; the locator provides the asset-specific path. You need both, and the endpoint must be running. ## What comes next - [Transforms and jobs](/api-guides/how-to/media/transforms-and-jobs): deeper detail on jobs and presets. - [Content protection](/api-guides/how-to/media/content-protection): add DRM to the published output. - [Webhooks](/api-guides/understanding/webhooks): replace polling with event-driven completion. # Understand the APIs These guides explain the conventions shared across the MK.IO APIs, including authentication, pagination, rate limits, errors, and resource states. - [API overview](/api-guides/understanding/overview): Learn the shared base URL, authentication, list responses, and resource states. - [Pagination](/api-guides/understanding/pagination): Understand how list responses expose pages of resources. - [Rate limits](/api-guides/understanding/rate-limits): Learn how the APIs communicate request limits. - [Error handling](/api-guides/understanding/error-handling): Interpret API errors and handle failed requests. - [Resource states](/api-guides/understanding/resource-states): Track the state of resources and long-running operations. - [Webhooks](/api-guides/understanding/webhooks): Understand event notifications from the platform. # Error handling The MK.IO APIs return the same error body for every failure. Once your client handles that one shape, the rest of error handling is consistent across the platform: inspect the HTTP status, branch on the machine-readable code, log the human-readable detail, and keep the request reference for follow-up. ## The standard error body When an operation fails, the API returns a JSON object with this shape: ```json { "error": { "code": "", "detail": "", "extraDetail": {} }, "status": 400, "ref": "" } ``` Each field has a distinct job: | Field | Use it for | | :--- | :--- | | `error.code` | Programmatic branching in your application. | | `error.detail` | Logs, dashboards, and operator-facing messages. | | `error.extraDetail` | Any additional context the API includes. | | `status` | The HTTP status code, repeated in the body. | | `ref` | Support follow-up and request tracing. | If you keep only one field beyond the status line, keep `ref`. It is the quickest way to identify a failing request afterwards. ## Status codes across the platform The exact response set varies by endpoint, but these codes appear throughout the APIs. Success responses: | Code | Meaning | | :--- | :--- | | `200` | The request succeeded and returned a body. | | `201` | The resource was created. | | `202` | The request was accepted and continues asynchronously. | | `204` | The request succeeded with no response body. | Client-side problems: | Code | Meaning | What to do next | | :--- | :--- | :--- | | `400` | Bad Request | Recheck the path, query parameters, and body format. | | `401` | Unauthorized | Recheck the bearer token and `Authorization` header. | | `403` | Forbidden | Recheck organization, project, and operation access for the token's user. | | `404` | Not Found | Recheck names and path parameters, especially project-scoped names. | | `409` | Conflict | Recheck the resource state, or whether the operation conflicts with an existing reference. | | `429` | Too Many Requests | Back off and retry later. See [Rate limits](/api-guides/understanding/rate-limits). | Server-side problems: | Code | Meaning | What to do next | | :--- | :--- | :--- | | `500` | Internal Server Error | Retry carefully and keep the `ref` value. | | `503` | Service Unavailable | Treat it as temporary and keep the `ref` value if it persists. | ## A handling order that works When a request fails: 1. Record the HTTP status code. 2. Parse `error.code` and `error.detail`. 3. Record `ref`. 4. Decide whether to fix the request, retry it, or surface it to an operator. As a guide to that decision: - `400`, `401`, `403`, and many `404` responses mean the request needs correcting. - `409` usually means the operation is valid, but the target resource is not in the right state yet, or is still referenced elsewhere. - `429`, `500`, and `503` are the cases where careful retry logic pays off. ## See the status and body together When you debug from the command line, print the transport status alongside the body so a log line captures both: ```bash curl -sS \ -H "Authorization: Bearer " \ -H "Accept: application/json" \ -w "\nHTTP Status: %{http_code}\n" \ "https://app.mk.io/api/v1/projects//media/assets/nonexistent" ``` # API overview The MK.IO APIs are separated by product area, but they share one operating model. Once you know how paths are scoped, how list responses are shaped, and how long-running resources expose state, you can move between the APIs without learning a new set of conventions each time. ## One base URL All MK.IO APIs share the same base URL: ```text https://app.mk.io ``` The resource path is what differs between them: | API | Path prefix | Scope | | :--- | :--- | :--- | | Media API | `/api/v1/projects/{project_name}/media/` | Project | | Management API | `/api/v1/` | Organization and project | | Fleets API | `/api/v1/projects/{project_name}/fleet/` | Project | | Infrastructure API | `/api/v1/projects/{project_name}/infra/` | Project | When a path includes `/projects/{project_name}/`, the request is project-scoped. When it does not, it usually targets organization-level or user-level configuration through the Management API. ## Authentication is shared The APIs use bearer authentication: ```http Authorization: Bearer ``` A single token works across setup and operational workflows. The same token can create a project through the Management API and then list assets through the Media API, as long as the issuing user has access to both operations. See [Authentication](/api-guides/getting-started/authentication) for how to create one. ## Requests are HTTP plus JSON The request shape follows a small set of rules: - `GET` reads a resource or lists a collection. - `PUT` creates or replaces a resource whose name is part of the URL. - `PATCH` applies a partial update where the endpoint supports it. - `POST` runs an action, such as `start`, `stop`, `allocate`, `scale`, `backup`, or `restore`. - `DELETE` removes a resource. Send `Content-Type: application/json` on any request that has a JSON body. ## List responses share a shape A `list` endpoint returns the resources in `value` and metadata in `supplemental`: - `value`: the array of resources on the current page. - `supplemental`: counts and pagination metadata, including `supplemental.pagination` with `start`, `end`, `records`, and `total`. That shared shape gives you one client pattern for every collection: read `value`, then use `supplemental.pagination` to decide whether to request the next page. See [Pagination and filtering](/api-guides/understanding/pagination) for the query parameters and the paging mechanism. ## Many resources expose a lightweight state endpoint Several resources expose a `/state` endpoint next to the main resource, including assets, content key policies, jobs, live events, live outputs, streaming endpoints, and streaming locators. These endpoints let you monitor a long-running resource without retrieving the full object each time. See [Resource states](/api-guides/understanding/resource-states). ## Long-running workflows are common Several important workflows are not single calls: - A **job** moves from queued work, through processing, to a terminal state. - A **live event** is created, then started, then paired with one or more live outputs. - A **streaming endpoint** is created and started before playback works. Designing around these workflows means combining list or get operations with either polling on `/state` or subscribing to [webhooks](/api-guides/understanding/webhooks). ## Where to go next - [Authentication](/api-guides/getting-started/authentication): create a token and make your first call. - [Error handling](/api-guides/understanding/error-handling): the shared error body and status codes. - [Pagination and filtering](/api-guides/understanding/pagination): page and narrow list responses. - [Resource states](/api-guides/understanding/resource-states): read lifecycle state and design around it. # Pagination and filtering Most MK.IO `list` endpoints accept query parameters that page, sort, and filter the results on the server. Using them keeps responses small, removes client-side filtering work, and reduces the number of follow-up requests your integration makes. ## The shape of a list response A list endpoint returns two top-level fields: - `value`: the array of resources on the current page. - `supplemental`: metadata about the result set, including pagination counts. A trimmed response looks like this: ```json { "value": [ { "name": "asset-001" }, { "name": "asset-002" } ], "supplemental": { "count": 2, "kind": "Asset", "operation": "list", "pagination": { "start": 0, "end": 2, "records": 2, "total": 145 } } } ``` The `pagination` block tells you where you are in the collection: `records` is how many items this page returned, and `total` is how many exist across the whole project. Comparing them tells you whether more pages remain. ## Limit a page with $top `$top` caps how many items a single page returns. The service returns up to that many, and never more than exist. ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$top=10" \ -H "Authorization: Bearer " ``` The `$` is escaped as `\$` in these examples so that your shell does not treat the parameter as a variable. ## Page through results with $skiptoken `$skiptoken` sets the start offset for the next page. Use it to walk a collection one page at a time. ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$top=10&\$skiptoken=" \ -H "Authorization: Bearer " ``` Do not construct `$skiptoken` values by hand. Take the value from the previous response. To page until the end, request pages until `supplemental.pagination.records` is smaller than your `$top`, or until the running total of returned records reaches `supplemental.pagination.total`. ## Sort with $orderby `$orderby` orders the result collection by a field. The valid fields depend on the endpoint. ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$orderby=properties/created%20desc" \ -H "Authorization: Bearer " ``` For the Media API asset list, sortable fields include `name`, `properties/created`, `properties/lastModified`, and `properties/storageAccountName`. Other APIs expose their own sort keys. Check the [API reference](/api-reference/media-api) for the fields a given endpoint supports. ## Filter with $filter `$filter` restricts the result set to items that match an expression. ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$filter=name%20eq%20'my-asset'" \ -H "Authorization: Bearer " ``` The fields available to filter on vary by resource. Common examples are assets by `name` or `properties/created`, live events by `properties/resourceState`, devices by `spec/siteName`, and sites by `status/locationName`. ## Filter by label Several list endpoints also support label queries, which are separate from `$filter`. Return items that carry a given label key with `$label_key`: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$label_key=studio" \ -H "Authorization: Bearer " ``` When you pass more than one `$label_key`, an item must carry all of those keys to match. Match a key and value with `$label`. Use `=` for an exact match and `~` for a partial match: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$label=studio=paravalley" \ -H "Authorization: Bearer " ``` Label queries are supported on the asset, live event, device, network, and site list endpoints. ## Combine parameters to do less work The parameters compose. A single request can limit, sort, and filter at once: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$top=10&\$orderby=properties/created%20desc&\$label=studio=paravalley" \ -H "Authorization: Bearer " ``` That one call returns the ten most recent assets for one studio, which would otherwise take a full list plus client-side sorting and filtering. ## A pattern for scanning large collections When you need to process an entire collection, work from narrow to broad: 1. Apply the narrowest `$filter` or label query the task allows. 2. Add `$orderby` if processing order matters. 3. Set a bounded `$top`. 4. Page with `$skiptoken` until no further records remain. Filtering and sorting on the server is almost always better than listing everything and filtering locally. It returns less data, needs fewer follow-up requests, and keeps you clear of the [rate limits](/api-guides/understanding/rate-limits). ## Related reading - [API overview](/api-guides/understanding/overview): the request and response conventions these endpoints share. - [Rate limits](/api-guides/understanding/rate-limits): why server-side filtering matters for request volume. # Rate limits The MK.IO APIs limit request rates to protect platform stability. Most integrations never reach those limits during normal use. Background workers, tight polling loops, and bulk scans are the patterns that approach them, so they are worth designing carefully. ## What a 429 looks like When you exceed a limit, the API responds with `429 Too Many Requests`. The body uses the same standard error shape as any other failure: ```json { "error": { "code": "", "detail": "", "extraDetail": {} }, "status": 429, "ref": "" } ``` Record the `ref` value before you retry. If a `429` is unexpected for your request volume, `ref` is the fastest way to identify the request later. See [Error handling](/api-guides/understanding/error-handling) for the full error model. ## Published limits Most endpoints enforce these limits: | Request type | Limit | | :--- | :--- | | `GET` | 2,000 per minute | | `DELETE`, `PATCH`, `POST`, `PUT` | 1,000 per minute | Some endpoints, including authentication flows, apply stricter limits. Limits are scoped to the resource the path targets. Project-scoped endpoints (those with `/projects/{project_name}/` in the path) count per project. Endpoints without a project in the path count per organization. This matters for multi-project automation: a process that fans out across projects behaves differently from one that repeatedly targets a single project. ## The most common way to hit a limit Aggressive polling is the usual cause. Polling 50 jobs once per second is 3,000 `GET` requests per minute against one project, which exceeds the read limit before any list or detail requests are counted. This is why event-driven workflows scale better than timer-based status checks. ## Design within the limits ### Prefer events over polling Where an operation has webhook coverage, subscribe instead of polling. Use `MediaKind.JobFinished` rather than polling a job, and use channel state events for live workflows. One event delivery replaces many repeated reads. See [Webhooks](/api-guides/understanding/webhooks). ### Narrow list requests on the server Use `$filter`, `$label`, `$label_key`, and `$top` so you request only what you need: ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/assets?\$top=20&\$label=studio=paravalley" \ -H "Authorization: Bearer " ``` This returns far fewer items than listing everything and filtering locally. See [Pagination and filtering](/api-guides/understanding/pagination). ### Page forward instead of restarting scans When you scan a large collection, continue from your last `$skiptoken` rather than rerunning the first page from the top. ### Back off between retries When you retry after a temporary failure, add a delay between attempts rather than retrying immediately. Even a fixed delay helps prevent a brief problem from becoming a sustained rate-limit issue. ## A design pattern for long-running workflows 1. Create the resource. 2. If webhook events exist for it, wait for the event. 3. If you must poll, poll the narrowest endpoint available, such as `/state`. 4. Add a delay between attempts. 5. Stop polling once the resource reaches the state you need. This keeps request volume predictable and usually improves end-to-end latency. ## If you keep hitting the limit If a normal workflow returns `429` consistently after you have narrowed requests and reduced polling, collect the project or organization context, the endpoint being called, the approximate request rate, and the `ref` values from the failing responses. That gives you enough to investigate the traffic pattern rather than guess. # Resource states Many MK.IO resources are asynchronous. A request to create or start one returns quickly, but the resource then moves through internal processing before it is ready to use. An integration that ignores state eventually tries to publish, stream, or delete a resource at the wrong moment. ## Where state appears State is exposed in two ways: - A dedicated `/state` endpoint next to the resource, for lightweight polling. - State fields on the full resource record, returned by a normal `GET`. In the Media API, the resources with a `/state` endpoint are assets, content key policies, jobs, live events, live outputs, streaming endpoints, and streaming locators. Poll the `/state` endpoint when you only need the current status. Read the full resource when you also need its configuration. ## Read the /state endpoint The `/state` endpoint returns a small, uniform body. The `state` value is a string whose meaning depends on the resource type. ```bash curl -X GET "https://app.mk.io/api/v1/projects//media/transforms//jobs//state" \ -H "Authorization: Bearer " ``` ```json { "status": { "state": "Processing" } } ``` ## The states each resource reports Each resource moves through its own lifecycle. The table below lists the states you observe in normal operation, taken from the MK.IO [resource states reference](/mkio/reference/resource-states). Use them to decide when the next step in a workflow is valid. | Resource | Initial | Ready | Running lifecycle | | :--- | :--- | :--- | :--- | | Asset | `Pending` | `Ready` | - | | Content key policy | `Creating` | `Created` | - | | Live output | `Creating` | `Running` | - | | Streaming locator | `Creating` | `Created` | - | | Streaming endpoint | `Creating` | `Created` | `Starting`, `Running`, `Stopping`, `Stopped` | | Live event | `Allocating` | `StandBy` | `Starting`, `Running`, `Stopping`, `Stopped` | Jobs use a separate set of states, reported in `properties.state`: `Queued`, `Scheduled`, `Processing`, `Finished`, `Canceling`, `Canceled`, and `Error`. Two states apply to almost every resource: - `Deleting`: the resource is deleted and the system is processing the removal. - `Deleted`: removal is complete. You rarely observe this value, because the resource no longer exists and the endpoint returns `404`. The API may also report extra transient states for some resources, such as `Updating`, `Scaling`, or `Error`. The [Media API reference](/api-reference/media-api) lists the complete enum for each resource, while the [resource states reference](/mkio/reference/resource-states) explains what each value means. ## Tell runtime state from provisioning state Live events, live outputs, streaming endpoints, and streaming locators expose two separate ideas of state: - `provisioningState` reports whether the control plane finished creating or updating the resource. Its values are `InProgress`, `Succeeded`, and `Failed`. - `resourceState` reports whether the resource is currently running, stopped, or being deleted. When you wait for a resource to become usable, do not stop at `provisioningState` of `Succeeded`. A streaming endpoint can be provisioned but not yet `Running`. Check `resourceState` as well. ## Design state transitions into the workflow A safe automation flow waits for the right state before each dependent call: 1. Create or start the resource. 2. Poll until it reaches the state that makes the next step valid. 3. Make the next call only then. In practice: - Wait for a job to reach `Finished` before you publish its output asset. - Wait for a live event to reach `Running` before you expect ingest to work. - Wait for a streaming endpoint to reach `Running` before you expect playback to succeed. ## When to stop polling Polling suits short waits and one-off scripts. It becomes expensive when you monitor many jobs or long-running live workflows, because each check is a request that counts against the [rate limits](/api-guides/understanding/rate-limits). At that scale, [webhooks](/api-guides/understanding/webhooks) deliver the same state changes without the repeated reads. A common pattern is to use webhooks for change notification and a single `GET` for current detail when a user asks for it. ## What comes next - [Webhooks](/api-guides/understanding/webhooks): replace repeated polling with event delivery. - [Transforms and jobs](/api-guides/how-to/media/transforms-and-jobs): see how job state fits a VOD workflow. - [Live streaming](/api-guides/how-to/media/live-streaming): see how live-event state affects ingest and publishing. # Webhooks Webhooks let your integration react to MK.IO events as they happen. Rather than polling jobs, live events, or streaming locators on a timer, you register a webhook rule and receive an HTTP `POST` request whenever a subscribed event fires. Webhook rules belong to the Management API. ## How a webhook rule works A rule belongs to a project and defines where to deliver events, whether delivery is on, and which events trigger it: - `url`: the target for the `POST` request. - `enabled`: whether the rule delivers. - `events`: the event types that trigger the rule. - `headers` and `queryParams`: non-sensitive request decoration. - `authentication.headers` and `authentication.queryParams`: sensitive values, such as a shared secret. The configuration endpoint is `/api/v1/projects/{project_name}/webhook/rules`. For the create, edit, and delete calls, see [Webhook rules](/api-guides/how-to/management/webhooks). ## The events you can subscribe to | Event | Use it to | | :--- | :--- | | `MediaKind.JobStarted` | Track when a job leaves the queue and begins processing. | | `MediaKind.JobFinished` | Trigger downstream work when a job reaches a terminal state. | | `MediaKind.StreamingLocatorCreated` | React when newly published content is ready. | | `MediaKind.ChannelInstanceStarted` | Track live channel startup. | | `MediaKind.ChannelInstanceStopped` | Track live channel shutdown. | | `MediaKind.ChannelInstanceError` | Alert on live channel failures. | | `MediaKind.ScheduledOperationAccepted` | Observe scheduled work entering the pipeline. | | `MediaKind.ScheduledOperationOngoing` | Track scheduled work in progress. | | `MediaKind.ScheduledOperationCompleted` | Trigger follow-up work after a scheduled operation finishes. | | `MediaKind.ScheduledOperationError` | Alert on scheduled-operation failures. | Scheduled operation events are available only when that feature is enabled for the organization. ## The delivered payload MK.IO delivers events in the [CloudEvents](https://cloudevents.io/) format. Every payload has the same envelope: ```json { "specversion": "1.0", "id": "", "type": "", "source": "", "time": "", "datacontenttype": "application/json", "data": { "projectName": "", "previousState": "", "state": "", "resource": {} } } ``` The `data.resource` object holds the full resource, exactly as a `GET` on `source` would return it. The `data.previousState` and `data.state` fields tell you which transition fired the event: | Event | `previousState` | `state` | | :--- | :--- | :--- | | `MediaKind.JobStarted` | `Queued` | `Processing` | | `MediaKind.JobFinished` | `Processing` | `Finished`, `Canceled`, or `Error` | | `MediaKind.StreamingLocatorCreated` | `Creating` | `Created` | ## What webhooks change in your design Without webhooks, a client creates a resource, then polls a `GET` or `/state` endpoint until the resource reaches a terminal state, handling retry timing and rate limits along the way. With webhooks the flow becomes: 1. Create the resource. 2. Return control to the user or job runner. 3. Wait for the event that reports the state change. 4. Retrieve the resource only when you need fresh detail. This is most valuable for jobs and live workflows, where the wait can be long and unpredictable. ## A useful first rule For a Video On Demand (VOD) workflow, the smallest useful subscription is `MediaKind.JobStarted` and `MediaKind.JobFinished`, which signal when a job begins and when it reaches a terminal state. For live workflows, start with `MediaKind.ChannelInstanceStarted`, `MediaKind.ChannelInstanceStopped`, and `MediaKind.ChannelInstanceError`. ## Authentication fields are write-only A rule separates general request decoration from sensitive values. `headers` and `queryParams` carry general fields. `authentication.headers` and `authentication.queryParams` carry secret or authentication values. Fields under `authentication` are write-only: when you read a saved rule back, those values appear masked rather than in clear text. ## Build a handler that survives retries If a delivery fails, MK.IO retries it. Your endpoint should therefore handle duplicate deliveries safely: - Make event handling idempotent where possible. - Log the event `id`, `type`, and `source`. - Return a success response as soon as you have safely accepted the event. Treat the endpoint as a security surface. Serve it over HTTPS, pass a shared secret through the `authentication` fields, and verify that secret in your handler before acting on a payload. ## When a direct read is still the right tool Webhooks suit asynchronous workflows, but they do not replace every read. Use a direct `GET` when a user asks for the current state now, when you need to list resources on demand, or when the resource has no event coverage. The strongest integrations combine both: webhooks for change notification, and API reads for current detail. ## Related reading - [Resource states](/api-guides/understanding/resource-states): the state values these events report. - [Rate limits](/api-guides/understanding/rate-limits): why event delivery scales better than polling. # Welcome to MK.IO Beam MK.IO Beam is engineered to meet the evolving demands of processing at the edge of your network, delivering high-quality broadcast and streaming headends, low latency contribution encoding and capable video/audio decoding. Its flexible architecture allows for seamless integration into existing workflows, providing a future-proof solution. MK.IO Beam connects to the cloud, enabling direct and automated control, monitoring and life cycle management via the MK.IO cloud portal for the ultimate in efficient operations. - [Quick Start](/beam/quick-start): Configure and register your device. - [Connect to MK.IO](/beam/connect-to-mkio): Check network requirements and find every device-management guide. - [Essentials UI](/beam/essentials): Monitor channels and create new services from the streamlined Essentials dashboard. - [Web Interface Overview](/beam/web-interface): Get familiar with the Advanced web interface.