Return Shapes

tell the AI exactly what each call gives back

Return Shapes

The single biggest source of AI mistakes is assuming a return shape. These are the
actual shapes each call returns.

get_asset(asset_id)dict | None

Key fields: id, parentId, displayName, assetType (int; 1 = folder), plus storage
/ status fields. Returns None if not found / request fails.

asset = sdk.get_asset(asset_id)
asset["id"]; asset["parentId"]; asset["assetType"]  # 1 == folder

get_asset_child_nodes(...)dict | None

{ "items": [ {id, displayName/name, assetType, hasChildren,...},... ], "totalItems": int }.
Page with page_index (0-based) and page_size; stop when a page has fewer than
page_size items.

create_folder_asset(parent_id, display_name)dict | None

{ "id": "<new-folder-id>" }. The new folder id is result["id"].

upload_asset(...)str | None (NOT a dict)

Returns the asset id as a bare string on success, None on failure.

asset_id = sdk.upload_asset(None, None, None, "replace", file_path, parent_id)
# asset_id is a string - do NOT do asset_id["id"]
⚠️

Inconsistent with other create-style methods that return {id}.

move_asset(...)dict | None

{ "items": [ {moved asset info} ], "totalItemCount": int }. The move is asynchronous:
the items[*] come back immediately as
{ "id", "assetId", "assetStatus": { "enumValue": 14, "description": "Moving",... } } - the
asset is still settling. Confirm the move by polling get_asset until parentId flips to
the destination, not by trusting the immediate response.

Request shape: { "targetIds": ["<asset-id>"], "actionArguments": { "destinationFolderAssetId": "<folder-id>" } }. Supplying actionArguments.name renames
the asset during the move
- the SDK's name positional maps to it.

delete_asset(asset_id)dict | None

A confirmation response (HTTP 200) on success, None on failure. The operation is
permanent. Confirm by get_asset no longer resolving the id.

search(...)

  • Pythondict | None.
  • JavaScript → the same object (or null on request failure). Empty results return the
    object with items: [] - JS no longer collapses zero hits to false, so read fields directly and guard only against null.

Top-level shape:

{
  "items": [ /* SearchResultModel objects */ ],
  "totalItemCount": 9,      // omit excludeTotalRecordCount to get this
  "nextPageOffset": null,   // real ListResultModel<T> field; nullable/omittable
  "hasItems": true          // present on the portal responses; cheap "any hits?" check
}
⚠️

The canonical paging fields on ListResultModel<T> are items, totalItemCount,

nextPageOffset, and hasItems. nextPageOffset
is a real field, but it's nullable/omittable - don't assume it's always present.
totalRecordCount is not a field. Page by sending the next pageOffset/pageSize
yourself (offset is zero-based); stop when len(items) < pageSize or
pageOffset*pageSize >= totalItemCount.

Each items[*] (SearchResultModel) carries flat metadata plus a nested identifiers
bag (the platform lower-cases its keys):

{
  "id": "73d0…", "masterId": "73d0…",
  "contentDefinitionId": "3ff2…", "contentDefinitionTitle": "Asset",
  "title": "BaseContent",
  "isRestricted": false, "isShared": false,
  "createdDate": "2022-09-17T03:02:13Z",
  "lastModifiedDate": "2024-03-30T20:04:52.47Z",
  "permission": 3, "inheritSecurity": false, "contentLanguageStatus": 0,
  "identifiers": {
    "displayName": "BaseContent", "name": "BaseContent",
    "parentId": "a732…", "assetType": 1, "displayPath": "BaseContent",
    "createdDate": "2022-09-17T03:02:13Z"
  }
}

The folder/asset name and parentId live under identifiers, not at the top level -
read item["identifiers"]["displayName"] / ["parentId"]. Selecting
search_result_fields=[{"name": "identifiers.displayName"}] (dotted) controls which of
these are populated.

get_asset_details(asset_id)dict | None

The full detail model. The id key is assetId, not id (unlike get_asset, which uses
id). Returns None if not found. Top-level keys:

{
  "assetId": "7fad…",
  "permission": 3, "isRestricted": false, "isShared": false, "inheritSecurity": true,
  "parents": [ { "id": "...", "displayName": "...", "assetType": 5 }, ... ],  // breadcrumb chain
  "relatedAssets": [ { "id", "url", "fullUrl", "title", "metadataType", "mediaType", "contentLength" }, ... ],
  "availability": [ { "availableStartDate": "...", "availableEndDate": "..." } ],
  "availabilityStatus": 3
}
details = sdk.get_asset_details(asset_id)
details["assetId"]  # NOT details["id"]
⚠️

get_assetid; get_asset_detailsassetId. Don't assume one from the other.

relatedAssets[*].fullUrl is a short-lived signed S3 URL (manifest/processor-jobs/
screenshots/transcripts) - fetch it promptly; don't cache it.

get_asset_parent_folders(asset_id, page_size)dict | None

{ "items": [ folder summaries ], "totalItemCount": int, "hasItems": bool }, root-first
(top-level folder → … → immediate parent). Each item is a folder-shaped SearchResultModel
(same identifiers bag as search), with identifiers.topLevelFolder: true on the root and
identifiers.parentId linking the chain. Useful for breadcrumbs.

create_placeholder_asset(parent_id, asset_name)dict | None

{ "id": "<new-asset-id>" }. The placeholder id is result["id"]. Uploading into that
placeholder (upload_asset(name, existing_asset_id=<placeholder-id>,...)) keeps the same
id
through the upload - confirmed live: the asset settles under the original parentId.

update_asset_language(asset_id, language_id)dict | None

Updated asset info on success, None on failure. Needs a real platform language_id. There
is no list-languages SDK verb - obtain the id via the search API, filtering on the
Languages content definition (contentDefinitionId Equals <languages-content-def-id>) and
reading the matching item's id. See recipes/search-patterns.md. May trigger reprocessing
of language-dependent AI metadata.

get_live_channelslist[dict] | None

A plain array of channel objects - not a {items} envelope. Iterate directly; an
empty deployment yields []. Each element is the same shape as get_live_channel below.

for ch in (sdk.get_live_channels() or []):
    ch["id"]; ch["name"]; ch["status"]["description"]  # status NAME, e.g. "Running"

get_live_channel(live_channel_id)dict | None

The full channel object; None if not found. The status is nested - the name is at
channel["status"]["description"], the lookup UUID at channel["status"]["id"]. Key
fields:

{
  "id": "f6d0…", "name": "Studio A",
  "type": { "id": "2bf0…", "description": "Normal" },   // LiveChannelType lookup
  "status": { "id": "26a7…405", "description": "Running" }, // LiveChannelStatus lookup
  "statusMessage": "…", "statusName": "Running",
  "associatedInputs": [ /* input refs */ ],
  "outputs": [ /* LiveChannelOutput */ ],
  "scheduleEvents": [ /* LiveChannelEvent */ ],
  "securityGroups": [ /* lookup items */ ]
}

Map status.description/type.description (or their .id UUIDs, or the int enums) via
reference/enums.md. Python has no public status verb - read status.description here
(JS exposes getLiveChannelStatus, which returns exactly that, or "Deleted").

get_live_inputs / get_live_input(id)dict | None

A collection (list) / single live input. Each input carries id, name, type, its own
status + statusMessage, sources, and destinations - independent of channel status.

get_live_operators / get_live_operator(id)dict | None

A collection (list) / single live operator. The operator id is the live channel id; the
object mirrors the channel's run-time status plus operator/segment metadata.

start_live_channel(...) / stop_live_channel(...) / live_channel_refreshNone

No return payload (the SDK methods are typed -> None); they raise on HTTP error. Confirm
the effect by re-reading get_live_channel(...)["status"]["description"] ("Running" after
start, "Idle" after stop). With wait_for_start/wait_for_stop, the call blocks until the
target status before returning.

create_content_group(name) / get_content_group(id)dict | None

The content group behind a UI collection (portal apiType only). The created object carries
its id; get_content_group returns the full record via the SDK's /api/contentGroup path:

Deployment note: some deployments do not expose the contentGroup/portal/groups routes
and return HTTP 404. The SDK swallows the 404 and returns None (no exception), so
create_content_group yields None with no object created. Reads tolerate None; run
collections on a deployment where the portal routes are present.

// create_content_group("Test Collection") response:
{ "id": "85a8…", "name": "Test Collection", "sharedUsers": [], "contents": [] }

// after add_contents_to_content_group, each contents[] item is {id, title, properties}
// (NOT a {id, description} LookupItem):
{
  "id": "<asset-uuid>", "title": "they_live.mp4",
  "properties": {
    "assetType": 2, "mediaTypeDisplay": "Video", "contentDefinitionTitle": "Asset",
    "contentDefinitionId": "3ff2…", "createdDate": "5/14/2026 6:23:04 PM",
    "previewImageUrl": "…", "previewImageFullUrl": "…"
  }
}

Membership lives under contents, each item {id, title, properties}. After
add_contents_to_content_group, re-read and check contents[*].id. The create response carries
only {id, name, sharedUsers, contents} - no shares/isShared/sequence (those are not
in the response payload).

get_content_groupsdict | None

The caller's content groups. The SDK method is typed dict | None, but its own docstring
example iterates the result as a list of {id, name} lookups
(for group in groups: group["id"]) - a type-hint/docstring mismatch; treat the payload as a
list of group lookups and read group["id"]/group["name"]. On deployments without the portal
route this returns None (see the deployment note above). Owner = the authenticated portal user.

get_portal_groups(portal_groups)dict | None

An object keyed by the requested bucket names (e.g. savedSearches, contentGroups,
sharedContentGroups) - pass a list of names, not ids.

content-group writes → None

add_contents_to_content_group, remove_contents_from_content_group, rename_content_group,
delete_content_group, share_content_group_with_users, and
stop_sharing_content_group_with_users are all typed -> None; they raise on HTTP error.
Confirm via a follow-up get_content_group (membership/name) or sharedUsers.

The API does return a body (the SDK discards it): add returns the full updated group (with
populated contents), while remove returns only { "id": "…" }. Don't rely on either - the
Python methods return None, so re-read with get_content_group.

get_content_definition(content_definition_id)dict | None

A single content definition (entity), including its field list. This is the discovery
source for a bulk import (recipes/bulk-import-events-from-spreadsheet.md): iterate
contentFields[] to learn each writable field's propertyName and type label.

{
  "contentDefinitionId": "412a30e3-73ee-4eae-b739-e1fc87601c7d",
  "properties": { /* CD-level settings */ },
  "contentFields": [
    {
      "contentFieldId": "3364…",
      "isReadOnly": false, "isInEditorForm": true,
      "properties": {
        "title": "Name", "propertyName": "Name",
        "fieldId": { "description": "Short Text", "id": "ea65…" },
        "isRequired": true
      }
    }
    // … one entry per field; `properties.propertyName` is the writable field name,
    // `properties.fieldId.description` is the type label (e.g. "Short Text", "Lookup").
  ]
}

Use contentFields[*].properties.propertyName to build the spreadsheet→field mapping and
fieldId.description to know which columns need a {id, description} reference shape. The
curated field tables in schema.md are the offline equivalent.

Event writes → create_content + update_content (not create_and_update_event)

Do not use create_and_update_event for event writes: its update path is a no-op, it
misspells the startDatetime/endDatetime keys, and it returns None on the validation
failure you hit without a required eventType. Create
events the contract way - create_content returns {contentId, …}, then update_content
persists the fields (immediately searchable by slug):

cid = sdk.create_content(EVENT_CD, None)["contentId"]
sdk.update_content(cid, EVENT_CD, {
    "name": name, "slug": slug,
    "eventType": {"id": et_id, "description": "Auto Scheduled"},  # REQUIRED reference
    "startDatetime": start, "endDatetime": end,                   # NOTE: lowercase 't'
}, None)
# update_content returns None even on success () - keep `cid` from create_content.

add_live_schedule_to_event(...) / delete_event(...)None

Both are typed -> None and act by side effect (they raise on HTTP error). After
add_live_schedule_to_event, re-read the event via get_content(event_id, EVENT_CD, None) and
inspect its liveChannel/schedule fields. After delete_event, re-search the event CD by
slug to confirm it is gone (deletion is async).


Did this page help you?