A channel is a public site where your organization publishes videos for its audience. The API reads three things about channels and writes one:
| Resource | What it is | Read | Write |
|---|---|---|---|
GET /v1/channels |
the organization’s channels | websites.catalogue.read |
— |
GET /v1/categories |
each channel’s tree of categories | websites.catalogue.read |
from Videas |
GET /v1/publications |
what each channel publishes | websites.catalogue.read |
websites.catalogue.publish |
On the keys screen, the two permissions are Read the channels through the API and Publish on the channels through the API. Neither implies the other, nor any permission of the library: a key that publishes does not edit an asset, and does not read the library.
Before you start
- Your organization must publish channels. For an organization that does not,
every endpoint of this page answers
403to all of its keys — as it would a key without these permissions. A key may carry them beforehand: it works with no change the day the organization has its channels. - An organization whose channels are still being moved to Videas reads them as
usual, but every write on them is refused with a
409until the move is complete: they are changed from the platform they are moving from until then. - The assets to publish are in the library: a file added and processed, or a video hosted elsewhere.
The channels
curl https://api.videas.com/v1/channels \
-H "Authorization: Bearer $VIDEAS_API_KEY"
{
"data": [
{
"id": "0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b",
"name": "Materials TV",
"url": "https://webtv.example.com",
"language": "en",
"created_at": "2026-02-10T09:00:00Z",
"updated_at": "2026-09-30T16:12:45Z"
}
],
"next": null
}
| Field | |
|---|---|
id |
The channel’s identifier — what the channel_id of a category, and of a publication, names. |
name |
Its name, as its audience reads it. |
url |
The address of its home page: its main domain, followed by a language segment such as /fr when that domain serves several channels, one per language. Every address on the channel starts with it. null while no domain of the channel is switched on. |
language |
The language it is written in, as a BCP 47 tag. |
created_at, updated_at |
Its creation and its last change. A change of its domains alone, which moves its url, does not move updated_at. |
The list carries every channel of the organization, online or not: url says
where the channel answers once it is online, not whether it is.
GET /v1/channels/count counts them, and GET /v1/channels/{channel_id} reads
one.
The categories
Each channel files its videos in a tree of categories, arranged from Videas and
read here. GET /v1/categories?channel_id= lists one channel’s — without
channel_id, every channel’s:
curl "https://api.videas.com/v1/categories?channel_id=0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b" \
-H "Authorization: Bearer $VIDEAS_API_KEY"
{
"data": [
{
"id": "0192a6a1-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"channel_id": "0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b",
"parent_id": null,
"name": "Processes",
"slug": "processes",
"position": 0,
"is_active": true,
"url": "https://webtv.example.com/categories/processes/",
"created_at": "2026-02-10T09:12:00Z",
"updated_at": "2026-02-10T09:12:00Z"
},
{
"id": "0192a6a1-2b3c-7d4e-9f5a-6b7c8d9e0f1a",
"channel_id": "0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b",
"parent_id": "0192a6a1-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"name": "Recycling",
"slug": "recycling",
"position": 1,
"is_active": true,
"url": "https://webtv.example.com/categories/recycling/",
"created_at": "2026-02-10T09:12:40Z",
"updated_at": "2026-02-10T09:12:40Z"
},
{
"id": "0192a6a1-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
"channel_id": "0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b",
"parent_id": null,
"name": "Events",
"slug": "events",
"position": 1,
"is_active": true,
"url": "https://webtv.example.com/categories/events/",
"created_at": "2026-02-10T09:13:05Z",
"updated_at": "2026-02-10T09:13:05Z"
}
],
"next": null
}
| Field | |
|---|---|
parent_id |
The category it sits under, of the same channel; null at the root. |
position |
Its place among the categories sharing its parent, lowest first. Not unique: at one position, the order is the names’. |
slug |
The handle its address is built on, unique within its channel. |
is_active |
Whether the audience sees it. A category switched off is hidden with everything below it; it keeps what is filed in it, and shows it again once switched back on. |
url |
The address of its page: the channel’s url followed by /categories/{slug}/; null when the channel’s is. It answers 404 while the category, or one above it, is switched off. |
updated_at |
Its last change, moving it under another parent included. |
Rebuild the tree
The list is flat, in the order the categories were created — not the order
the channel shows them in. Rebuild the tree from parent_id, and sort the
categories sharing a parent by position, then by name:
from collections import defaultdict
def category_tree(categories):
"""A channel's categories as a tree: [(category, [subtree…]), …]."""
children = defaultdict(list)
for category in categories:
children[category["parent_id"]].append(category)
def branch(parent_id):
siblings = sorted(children[parent_id], key=lambda c: (c["position"], c["name"]))
return [(category, branch(category["id"])) for category in siblings]
return branch(None)
A category switched off is in the list, and a publication may be filed in it —
which is how a team prepares a section before opening it. channel_id takes
one identifier; GET /v1/categories/count counts under the same filter.
The publications
A publication is one asset published on one channel, at its own address, and filed in that channel’s categories. An asset is published at most once on a channel.
{
"id": "0192a6e5-4c5d-7e6f-9a0b-1c2d3e4f5a6b",
"channel_id": "0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b",
"asset_id": "0192a6e4-3b5c-7d6e-8f90-a1b2c3d4e5f6",
"slug": "recycling-carbon-fibre-three-processes-compared",
"url": "https://webtv.example.com/recycling-carbon-fibre-three-processes-compared/",
"status": "published",
"published_at": "2026-10-15T08:30:21Z",
"is_listed": true,
"access": "public",
"category_ids": [
"0192a6a1-2b3c-7d4e-9f5a-6b7c8d9e0f1a",
"0192a6a1-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
],
"primary_category_id": "0192a6a1-2b3c-7d4e-9f5a-6b7c8d9e0f1a",
"created_at": "2026-10-15T08:30:21Z",
"updated_at": "2026-10-15T08:30:21Z"
}
| Field | |
|---|---|
channel_id, asset_id |
The channel and the asset. They never change. |
slug, url |
Its address: the channel’s url followed by /{slug}/; null when the channel’s is. |
status, published_at |
Where it is, and when it goes on the channel — see Status and scheduling. |
access |
Who it is for — see Who it is for. |
is_listed |
Whether the channel shows it in its catalog, its categories, its search and its sitemap. A publication that is not listed still answers at its url. |
category_ids |
The categories it is filed in — a set: its main category comes first when it has one, the others following in the order GET /v1/categories lists them. Empty when it is filed nowhere. |
primary_category_id |
Its main category, the one its page and its card name — one of category_ids; null when it has none. |
updated_at |
The last change of its own settings — its address, its status, its date, whether it is listed, who it is for. Filing it in other categories does not move it. |
List and find
GET /v1/publications lists every channel’s publications, whatever their status,
page by page, and GET /v1/publications/count counts them under the same
filters:
| Parameter | Keeps | Example |
|---|---|---|
channel_id |
the publications of this channel | ?channel_id=0192a6a0-… |
asset_id |
the publications of this asset — where it is published | ?asset_id=0192a6e4-… |
status |
these statuses, separated by commas | ?status=draft,published |
?asset_id= says where an asset is published; with channel_id beside it,
whether it is published on that channel — the answer then carries one
publication or none. Only publications whose asset is in GET /v1/assets are
listed.
Status and scheduling
status |
Meaning |
|---|---|
draft |
Not on the channel. |
published |
On the channel from its published_at — which may be still to come: a scheduled publication is published with a future date, and the channel shows it from that instant. |
archived |
Taken off the channel, and kept with its date. |
published_at is null for a draft never published; a draft or an archived
publication keeps the date it last went out with. Its url answers 404 until it
is published and its published_at has come. The list of statuses may grow:
read a value you do not know as a publication the channel does not show.
Who it is for
access says who the publication is for. It is read in five words:
access |
For whom |
|---|---|
public |
anybody the channel lets in |
authenticated |
anybody who signed in to the channel, or gave its password |
restricted |
only the members granted it |
paid |
the members who bought it, or hold a subscription covering it |
on_request |
the members who asked for it and were granted it |
And it is written in two: public and authenticated. The other three stand
for a right or an offer set from Videas: a request may neither set one (400) nor
move a publication off one (409), since leaving it would undo the offer or the
grant. The list may grow: read a value you do not know as restricted.
- It is the publication’s own setting, not what a visitor gets: on a channel
that asks every visitor to sign in, a
publicpublication is still behind that sign-in. - Left out of a creation, the channel decides: on a channel whose visitors
open an account themselves and whose site is otherwise open, a new publication
asks for that account (
authenticated); on any other, it is open to everyone the channel lets in (public). Sendaccessif you want to be sure of the result. - Created
authenticated— sent so, or given so by the channel —, a publication still shows itself, locked, to the visitors it is not for — in the channel’s listings when it is listed, and at its page —, and playing it asks them to sign in. Moved toauthenticatedlater by aPATCH, a publication createdpublicdoes not show itself so: those visitors do not see it at all. APATCHmoves the level alone, and nothing else.
Its address
slug is the handle the publication’s url is built on: lowercase letters and
digits in words joined by single hyphens, at most 255 characters, unique among the
channel’s publications.
- Left out of a creation, it is made from the asset’s own handle where it has
one, else from its name, followed by
-2,-3and on where the address is taken. - Sent, it is taken as it is:
400when the channel’s own site answers that address itself,409when another publication or a page of the channel holds it — the message names the address. - A new
slugleaves the old address answering, sending visitors to the new one.
Its filing
category_ids lists the channel’s categories the publication is filed in, and
primary_category_id its main category, which must be among them.
- At most 100 categories, all of the publication’s channel — switched-off ones included; an id sent twice counts once.
- It is a set. Read, its order says one thing only: the main category comes first. Sent, the order is ignored.
- Sent, the list replaces the set. A category the publication stays in keeps
the place and the featuring the team gave it there in Videas; a category it
enters puts it last;
[]files it nowhere. - The order inside a category and the featuring are set in Videas, not through the API.
Publish an asset
curl -X POST https://api.videas.com/v1/publications \
-H "Authorization: Bearer $VIDEAS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b",
"asset_id": "0192a6e4-3b5c-7d6e-8f90-a1b2c3d4e5f6",
"status": "published",
"access": "public",
"category_ids": [
"0192a6a1-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"0192a6a1-2b3c-7d4e-9f5a-6b7c8d9e0f1a"
],
"primary_category_id": "0192a6a1-2b3c-7d4e-9f5a-6b7c8d9e0f1a"
}'
| Field | |
|---|---|
channel_id |
Required. The channel. |
asset_id |
Required. The asset. It must be ready, and a channel publishes a video, a sound, a picture or a video hosted elsewhere — not a document. |
status |
draft, the default, or published. A publication is never created archived. |
published_at |
With status: published, when it goes on the channel (RFC 3339) — a future date schedules it. Absent: now. Refused on a draft. |
access |
public or authenticated. Absent: the channel’s choice. |
slug |
Its address. Absent: made from the asset. |
is_listed |
Whether the channel shows it in its listings — true unless you send false. |
category_ids, primary_category_id |
Its filing — each category takes it last. Absent: filed nowhere, with no main category. |
The answer is a 201, with Location: /v1/publications/{publication_id} and the
whole publication, as above.
- An asset not
readyyet — a file still being processed — is a409: send the request again once the asset is ready. A video hosted elsewhere is ready from the moment it exists. - An asset already published on this channel is a
409too:GET /v1/publications?channel_id=…&asset_id=…answers its publication.
Change a publication
PATCH /v1/publications/{publication_id} writes what you send, and leaves the
rest as it is. Its channel and its asset never change: sending them is a 422.
# Schedule it for 2 November, 9 a.m. Paris time
curl -X PATCH https://api.videas.com/v1/publications/0192a6e5-4c5d-7e6f-9a0b-1c2d3e4f5a6b \
-H "Authorization: Bearer $VIDEAS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "published", "published_at": "2026-11-02T08:00:00Z"}'
status: publishedputs it on the channel — at the date sent, else now: one taken off and put back on comes back as new, not under the date it first went out on.draftorarchivedtakes it off, keeping its date.published_atsent alone moves the date of a publication already published; on any other, or besidestatus: draftorarchived, it is a400.slugrenames it, and the old address sends visitors to the new one.category_idsreplaces its filing;primary_category_idchanges its main category, or removes it withnull. Left out, the main category stays while the publication is still filed there, and there is none otherwise.accessmoves it betweenpublicandauthenticated.is_listedshows it in the channel’s listings, or takes it out of them.- Only
primary_category_idtakesnull: on any other field,nullis a400.
The answer is the whole publication. A PATCH that changes nothing writes
nothing, updated_at included, and the last request to arrive wins.
Retire a publication
Nothing deletes a publication. To take it off the channel, archive it —
{"status": "archived"} —: it is kept with everything attached to it, and is
published again with a PATCH. To free its address, give it another slug.
Retrying safely
- A
PATCHis sent again as it is after a timeout or a5xx: sent twice, the second finds the publication in the state asked for already. POST /v1/publicationspublishes an asset once on a channel: sent again, it is refused with a409. After a timeout or a5xx, look for the publication the first request may have made withGET /v1/publications?channel_id=…&asset_id=…before sending it again.
End to end
This script syncs one video from a spreadsheet to a channel: it creates the YouTube video in the library, tags it, fills a “Useful link” field, and publishes it in two categories. Run a second time, it changes nothing: each step looks first for what exists, and each write states the state wanted rather than a change.
It needs a key carrying six permissions: reading the library, adding files to it, importing videos from other platforms, editing its assets, reading the channels and publishing on them.
import os
import requests
API = "https://api.videas.com/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['VIDEAS_API_KEY']}"
WORKSPACE_ID = "0192a6b1-7c3e-7d40-9a51-3f0e2b6c8d10"
CHANNEL_ID = "0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b"
USEFUL_LINK_FIELD_ID = "0192a680-1d2e-7f3a-8b4c-5d6e7f8a9b0c"
# One row of the spreadsheet.
VIDEO = {
"external_ref": "webtv/2026/carbon-fibre-recycling",
"youtube_url": "https://www.youtube.com/watch?v=Xk3Ua9yQe7M",
"tags": ["Carbon fibre", "Recycling"],
"useful_link": {
"url": "https://example.com/carbon-fibre-recycling",
"label": "The full report",
},
"categories": ["processes", "recycling"], # the slugs of the channel's categories
"main_category": "recycling",
}
def call(method, path, **kwargs):
response = session.request(method, f"{API}{path}", **kwargs)
if not response.ok:
error = response.json()
raise RuntimeError(f"{method} {path} → {response.status_code} {error['code']}: {error['message']}")
return response.json()
def all_pages(path, **params):
while True:
page = call("GET", path, params=params)
yield from page["data"]
if page["next"] is None:
return
params["after"] = page["next"]
def find_or_create_video(video):
"""This row's asset: found by its reference, else created."""
found = call("GET", "/assets", params={"external_ref": video["external_ref"]})["data"]
if found:
return found[0]
return call("POST", "/assets", json={
"workspace_id": WORKSPACE_ID,
"third_party_url": video["youtube_url"],
"external_ref": video["external_ref"],
})
def publish(asset_id, category_ids, main_category_id):
"""The asset's publication on the channel, in the state wanted."""
wanted = {
"status": "published",
"access": "public",
"category_ids": category_ids,
"primary_category_id": main_category_id,
}
found = call("GET", "/publications", params={"channel_id": CHANNEL_ID, "asset_id": asset_id})["data"]
if found:
return call("PATCH", f"/publications/{found[0]['id']}", json=wanted)
return call("POST", "/publications", json={"channel_id": CHANNEL_ID, "asset_id": asset_id, **wanted})
asset = find_or_create_video(VIDEO)
call("PUT", f"/assets/{asset['id']}/tags", json={"tags": VIDEO["tags"]})
call("PATCH", f"/assets/{asset['id']}/custom-fields", json={
"values": [{"field_id": USEFUL_LINK_FIELD_ID, "value": VIDEO["useful_link"]}],
})
categories = {c["slug"]: c["id"] for c in all_pages("/categories", channel_id=CHANNEL_ID)}
publication = publish(
asset["id"],
[categories[slug] for slug in VIDEO["categories"]],
categories[VIDEO["main_category"]],
)
print(publication["status"], publication["url"])
The first run creates the asset (201), sets its two tags, fills its link,
and publishes the video (201):
published https://webtv.example.com/recycling-carbon-fibre-three-processes-compared/
The second finds the asset by its external_ref and the publication by
?channel_id=…&asset_id=…: it sends neither POST /v1/assets nor
POST /v1/publications. Its three writes — the tags, the link, the publication —
send what the asset and the publication already hold, and write nothing: the
same answers, and no updated_at that moves. It prints the same line.
Two things to know before making it your sync:
- The script states the state wanted, and restores it: a category added by hand in Videas, or a publication archived since, is undone on the next run. Have it send only the fields your data source is the reference for.
- A row’s
POST /v1/assetsis never sent again once it is created, because the lookup byexternal_refcomes first. Without it, a retry would create a second asset — Retrying safely.
Errors
Every error has the same body — {"code": "…", "message": "…", "request_id": "…"}.
Branch on code; message is written for a human and may change.
| Status | code |
What happened |
|---|---|---|
400 |
validation |
Creation or change: status: archived on a creation, or a published_at beside a draft; a published_at alone on a publication that is not published; a slug that is malformed, too long, or used by the channel’s own site; an asset of a kind a channel does not publish; more than 100 category_ids, a category of another channel, or a main category that is not among them; an access other than public or authenticated; null on a field that cannot be emptied. Reads: a channel_id or an asset_id that is not one uuid, an unknown status, an unknown parameter. The message names the field. |
401 |
unauthorized |
No key, or a key that is unknown, expired or revoked. |
403 |
forbidden |
The key does not carry websites.catalogue.read (reads) or websites.catalogue.publish (writes) — or the organization does not publish channels. |
404 |
not_found |
No channel of GET /v1/channels, no asset of GET /v1/assets or no publication of GET /v1/publications has this id — in the path as in the body. |
409 |
conflict |
The asset is already published on this channel; it is not ready yet; the slug is taken by another publication or a page of the channel; access would move a publication that is restricted, paid or on_request; or the organization’s channels are still being moved to Videas. |
415 |
unsupported_media_type |
The body is not sent as application/json. |
422 |
validation |
channel_id or asset_id missing on a creation, or sent on a change; a field of the wrong type; a status or an access that names nothing; a field the body does not list. |
429 |
rate_limited |
Too many requests: wait the number of seconds Retry-After gives. |
Next steps
- Add a video hosted elsewhere and edit an asset — what the script above chains.
- Custom fields — a
publicfield is shown on the publication’s page. - Errors, limits and versions.
- The API reference, generated from its contract.