Aller au contenu principal

Publier sur une chaîne

Lisez les chaînes de votre organisation et leurs catégories, publiez un asset sur une chaîne avec POST /v1/publications — statut, programmation, accès, adresse, classement —, changez-le avec PATCH, et rejouez une synchronisation sans rien changer.

Une chaîne est un site public où votre organisation publie des vidéos pour son audience. L’API en lit trois choses et en écrit une :

Ressource Ce qu’elle est Lecture Écriture
GET /v1/channels les chaînes de l’organisation websites.catalogue.read —
GET /v1/categories l’arbre de catégories de chaque chaîne websites.catalogue.read depuis Videas
GET /v1/publications ce que chaque chaîne publie websites.catalogue.read websites.catalogue.publish

Les deux permissions s’appellent, dans l’écran des clés, Lire les chaînes par l’API et Publier sur les chaînes par l’API. Aucune n’implique l’autre, ni aucune permission de la bibliothèque : une clé qui publie ne modifie pas un asset, et ne lit pas la bibliothèque.

Avant de commencer

  • Votre organisation doit publier des chaînes. Pour une organisation qui n’en publie pas, chaque endpoint de cette page répond 403 à toutes ses clés — comme à une clé sans ces permissions. Une clé peut les porter d’avance : elle fonctionne sans rien changer le jour où l’organisation a ses chaînes.
  • Une organisation dont les chaînes sont en cours de transfert vers Videas les lit normalement, mais chaque écriture y est refusée par un 409 jusqu’à la fin du transfert : elles se modifient depuis la plateforme d’où elles viennent tant qu’il n’est pas terminé.
  • Les assets à publier sont dans la bibliothèque : un fichier ajouté et traité, ou une vidéo hébergée ailleurs.

Les chaînes

curl https://api.videas.com/v1/channels \
  -H "Authorization: Bearer $VIDEAS_API_KEY"
{
  "data": [
    {
      "id": "0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b",
      "name": "Matériaux TV",
      "url": "https://webtv.example.com",
      "language": "fr",
      "created_at": "2026-02-10T09:00:00Z",
      "updated_at": "2026-09-30T16:12:45Z"
    }
  ],
  "next": null
}
Champ
id L’identifiant de la chaîne — ce que nomment le channel_id d’une catégorie et celui d’une publication.
name Son nom, tel que le lit son audience.
url L’adresse de sa page d’accueil : son domaine principal, suivi d’un segment de langue comme /fr quand ce domaine sert plusieurs chaînes, une par langue. Toutes les adresses de la chaîne commencent par elle. null tant qu’aucun domaine de la chaîne n’est activé.
language La langue dans laquelle elle est écrite, en balise BCP 47.
created_at, updated_at Sa création et sa dernière modification. Un changement de ses seuls domaines, qui déplace son url, ne fait pas bouger updated_at.

La liste porte toutes les chaînes de l’organisation, en ligne ou non : url dit où la chaîne répond une fois en ligne, pas si elle l’est. GET /v1/channels/count les compte, et GET /v1/channels/{channel_id} en lit une.

Les catégories

Chaque chaîne range ses vidéos dans un arbre de catégories, qui s’organise depuis Videas et se lit ici. GET /v1/categories?channel_id= liste celles d’une chaîne — sans channel_id, celles de toutes les chaînes :

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": "Procédés",
      "slug": "procedes",
      "position": 0,
      "is_active": true,
      "url": "https://webtv.example.com/categories/procedes/",
      "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": "Recyclage",
      "slug": "recyclage",
      "position": 1,
      "is_active": true,
      "url": "https://webtv.example.com/categories/recyclage/",
      "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": "Salons",
      "slug": "salons",
      "position": 1,
      "is_active": true,
      "url": "https://webtv.example.com/categories/salons/",
      "created_at": "2026-02-10T09:13:05Z",
      "updated_at": "2026-02-10T09:13:05Z"
    }
  ],
  "next": null
}
Champ
parent_id La catégorie sous laquelle elle se trouve, de la même chaîne ; null à la racine.
position Sa place parmi les catégories de même parent, la plus basse d’abord. Pas unique : à position égale, l’ordre est celui des noms.
slug Le nom sur lequel son adresse est construite, unique dans sa chaîne.
is_active Si l’audience la voit. Une catégorie désactivée est masquée avec tout ce qu’elle contient ; elle garde ce qui y est classé et le remontre une fois réactivée.
url L’adresse de sa page : l’url de la chaîne suivie de /categories/{slug}/ ; null quand celle de la chaîne l’est. Elle répond 404 tant que la catégorie, ou l’une de celles au-dessus, est désactivée.
updated_at Sa dernière modification, déplacement sous un autre parent compris.

Reconstruire l’arbre

La liste est plate, dans l’ordre de création des catégories — ce n’est pas l’ordre dans lequel la chaîne les montre. Reconstruisez l’arbre à partir de parent_id, et triez les catégories de même parent par position, puis par name :

from collections import defaultdict

def category_tree(categories):
    """Les catégories d'une chaîne en arbre : [(catégorie, [sous-arbre…]), …]."""
    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)

Une catégorie désactivée figure dans la liste, et une publication peut y être classée — c’est ainsi qu’une équipe prépare une rubrique avant de l’ouvrir. channel_id ne prend qu’un identifiant ; GET /v1/categories/count compte sous le même filtre.

Les publications

Une publication est un asset publié sur une chaîne, à sa propre adresse, et classé dans les catégories de cette chaîne. Un asset est publié au plus une fois par chaîne.

{
  "id": "0192a6e5-4c5d-7e6f-9a0b-1c2d3e4f5a6b",
  "channel_id": "0192a6a0-5d6e-7f80-9a1b-2c3d4e5f6a7b",
  "asset_id": "0192a6e4-3b5c-7d6e-8f90-a1b2c3d4e5f6",
  "slug": "recycler-la-fibre-de-carbone-trois-procedes-compares",
  "url": "https://webtv.example.com/recycler-la-fibre-de-carbone-trois-procedes-compares/",
  "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"
}
Champ
channel_id, asset_id La chaîne et l’asset. Ils ne changent jamais.
slug, url Son adresse : l’url de la chaîne suivie de /{slug}/ ; null quand celle de la chaîne l’est.
status, published_at Où elle en est, et quand elle passe sur la chaîne — voir Statut et programmation.
access À qui elle est destinée — voir Qui y a accès.
is_listed Si la chaîne la montre dans son catalogue, ses catégories, sa recherche et son plan de site. Une publication non listée répond quand même à son url.
category_ids Les catégories où elle est classée — un ensemble : sa catégorie principale vient d’abord quand elle en a une, les autres suivent dans l’ordre où GET /v1/categories les liste. Vide si elle n’est classée nulle part.
primary_category_id Sa catégorie principale, celle que nomment sa page et sa carte — l’une de category_ids ; null si elle n’en a pas.
updated_at La dernière modification de ses propres réglages — son adresse, son statut, sa date, sa visibilité dans les listes, son accès. La classer dans d’autres catégories ne le fait pas bouger.

Lister et retrouver

GET /v1/publications liste les publications de toutes les chaînes, quel que soit leur statut, page par page, et GET /v1/publications/count les compte sous les mêmes filtres :

Paramètre Garde Exemple
channel_id les publications de cette chaîne ?channel_id=0192a6a0-…
asset_id les publications de cet asset — où il est publié ?asset_id=0192a6e4-…
status ces statuts, séparés par des virgules ?status=draft,published

?asset_id= dit où un asset est publié ; avec channel_id à côté, s’il est publié sur cette chaîne — la réponse porte alors une publication ou aucune. Seules figurent les publications dont l’asset est dans GET /v1/assets.

Statut et programmation

status Signification
draft Pas sur la chaîne.
published Sur la chaîne à partir de son published_at — qui peut être à venir : une publication programmée est published avec une date future, et la chaîne la montre à partir de cet instant.
archived Retirée de la chaîne, et conservée avec sa date.

published_at vaut null pour un brouillon jamais publié ; un brouillon ou une publication archivée garde la date de sa dernière sortie. Son url répond 404 tant qu’elle n’est pas published et que son published_at n’est pas arrivé. La liste des statuts peut s’allonger : lisez une valeur inconnue comme une publication que la chaîne ne montre pas.

Qui y a accès

access dit à qui la publication est destinée. Il se lit en cinq mots :

access Pour qui
public quiconque la chaîne laisse entrer
authenticated quiconque s’est connecté à la chaîne, ou a donné son mot de passe
restricted les seuls membres à qui elle a été accordée
paid les membres qui l’ont achetée, ou dont l’abonnement la couvre
on_request les membres qui l’ont demandée et l’ont obtenue

Et il s’écrit en deux : public et authenticated. Les trois autres correspondent à un droit ou à une offre réglés depuis Videas : une requête ne peut ni en poser un (400), ni en faire sortir une publication (409), puisque la quitter déferait l’offre ou l’accès accordé. La liste peut s’allonger : lisez une valeur inconnue comme restricted.

  • C’est le réglage propre de la publication, pas ce qu’obtient un visiteur : sur une chaîne qui demande à chaque visiteur de se connecter, une publication public reste derrière cette connexion.
  • Sans access à la création, la chaîne décide : sur une chaîne dont les visiteurs ouvrent eux-mêmes un compte et dont le site est par ailleurs ouvert, une nouvelle publication demande ce compte (authenticated) ; sur toute autre, elle est ouverte à tous ceux que la chaîne laisse entrer (public). Envoyez access si vous voulez être sûr du résultat.
  • Créée authenticated — que la requête l’ait demandé ou que la chaîne en ait décidé —, une publication se montre quand même, verrouillée, aux visiteurs à qui elle n’est pas destinée — dans les listes de la chaîne si elle est listée, et à sa page —, et la lire leur demande de se connecter. Passée à authenticated plus tard par un PATCH, une publication créée public ne se montre pas ainsi : ces visiteurs ne la voient pas du tout. Un PATCH ne déplace que le niveau d’accès, rien d’autre.

Son adresse

slug est le nom sur lequel l’url de la publication est construite : des mots en minuscules et en chiffres reliés par des tirets simples, 255 caractères au plus, unique parmi les publications de la chaîne.

  • Absent à la création, il est tiré de l’adresse propre de l’asset s’il en a une, sinon de son titre, suivi de -2, -3, etc. là où l’adresse est déjà prise.
  • Envoyé, il est pris tel quel : 400 si le site de la chaîne répond lui-même à cette adresse, 409 si une autre publication ou une page de la chaîne la tient — le message nomme l’adresse.
  • Un nouveau slug laisse l’ancienne adresse répondre, en renvoyant les visiteurs vers la nouvelle.

Son classement

category_ids liste les catégories de la chaîne où la publication est classée, et primary_category_id sa catégorie principale, qui doit en faire partie.

  • 100 catégories au plus, toutes de la chaîne de la publication — désactivées comprises ; un identifiant envoyé deux fois compte une fois.
  • C’est un ensemble. À la lecture, son ordre ne dit qu’une chose : la catégorie principale vient d’abord. À l’envoi, l’ordre est ignoré.
  • Envoyée, la liste remplace l’ensemble. Une catégorie où la publication reste garde la place et la mise en avant que l’équipe lui y a données dans Videas ; une catégorie où elle entre la place en dernier ; [] ne la classe nulle part.
  • L’ordre à l’intérieur d’une catégorie et la mise en avant se règlent dans Videas, pas par l’API.

Publier un 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"
  }'
Champ
channel_id Obligatoire. La chaîne.
asset_id Obligatoire. L’asset. Il doit être ready, et une chaîne publie une vidéo, un son, une image ou une vidéo hébergée ailleurs — pas un document.
status draft, la valeur par défaut, ou published. Une publication ne naît jamais archived.
published_at Avec status: published, quand elle passe sur la chaîne (RFC 3339) — une date future la programme. Absente : maintenant. Refusée sur un brouillon.
access public ou authenticated. Absent : le choix de la chaîne.
slug Son adresse. Absent : tirée de l’asset.
is_listed Si la chaîne la montre dans ses listes — true sauf si vous envoyez false.
category_ids, primary_category_id Son classement — chaque catégorie la reçoit en dernier. Absents : classée nulle part, sans catégorie principale.

La réponse est un 201, avec Location: /v1/publications/{publication_id} et la publication entière, comme plus haut.

  • Un asset pas encore ready — un fichier encore en traitement — vaut un 409 : renvoyez la requête une fois l’asset prêt. Une vidéo hébergée ailleurs est prête dès sa création.
  • Un asset déjà publié sur cette chaîne vaut un 409 lui aussi : GET /v1/publications?channel_id=…&asset_id=… répond sa publication.

Modifier une publication

PATCH /v1/publications/{publication_id} écrit ce que vous envoyez, et laisse le reste tel quel. Sa chaîne et son asset ne changent jamais : les envoyer vaut un 422.

# La programmer pour le 2 novembre à 9 h, heure de Paris
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: published la met sur la chaîne — à la date envoyée, sinon maintenant : une publication retirée puis remise revient comme nouvelle, et non à la date de sa première sortie. draft ou archived l’en retire, en gardant sa date.
  • published_at envoyé seul déplace la date d’une publication déjà publiée ; sur une autre, ou à côté de status: draft ou archived, il vaut un 400.
  • slug la renomme, et l’ancienne adresse renvoie vers la nouvelle.
  • category_ids remplace son classement ; primary_category_id change sa catégorie principale, ou l’enlève avec null. Absent, la catégorie principale reste la même tant que la publication y est encore classée, et il n’y en a plus sinon.
  • access la déplace entre public et authenticated.
  • is_listed la montre dans les listes de la chaîne, ou l’en retire.
  • Seul primary_category_id accepte null : sur tout autre champ, null vaut un 400.

La réponse est la publication entière. Un PATCH qui ne change rien n’écrit rien, updated_at compris, et la dernière requête arrivée l’emporte.

Retirer une publication

Rien ne supprime une publication. Pour la retirer de la chaîne, archivez-la — {"status": "archived"} — : elle est conservée avec tout ce qui s’y rattache, et se republie d’un PATCH. Pour libérer son adresse, donnez-lui un autre slug.

Relancer sans risque

  • PATCH se renvoie tel quel après une expiration ou une 5xx : envoyé deux fois, le second trouve la publication déjà dans l’état demandé.
  • POST /v1/publications publie un asset une seule fois sur une chaîne : renvoyé, il est refusé par un 409. Après une expiration ou une 5xx, cherchez la publication que la première requête a pu créer avec GET /v1/publications?channel_id=…&asset_id=… avant de la renvoyer.

De bout en bout

Ce script synchronise une vidéo d’un tableur vers une chaîne : il crée la vidéo YouTube dans la bibliothèque, la tague, remplit un champ « Lien utile », et la publie dans deux catégories. Lancé une seconde fois, il ne change rien : chaque étape cherche d’abord ce qui existe, et chaque écriture dit l’état voulu plutôt qu’un changement.

Il lui faut une clé qui porte six permissions : lire la bibliothèque, y ajouter des fichiers, importer des vidéos d’autres plateformes, modifier ses contenus, lire les chaînes et y publier.

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"

# Une ligne du tableur.
VIDEO = {
    "external_ref": "webtv/2026/recyclage-fibre-carbone",
    "youtube_url": "https://www.youtube.com/watch?v=Xk3Ua9yQe7M",
    "tags": ["Fibre de carbone", "Recyclage"],
    "useful_link": {
        "url": "https://example.com/recyclage-fibre-carbone",
        "label": "Le dossier complet",
    },
    "categories": ["procedes", "recyclage"],  # les slugs des catégories de la chaîne
    "main_category": "recyclage",
}


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):
    """L'asset de cette ligne : retrouvé par sa référence, sinon créé."""
    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):
    """La publication de l'asset sur la chaîne, dans l'état voulu."""
    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"])

Le premier passage crée l’asset (201), pose ses deux tags, remplit son lien, et publie la vidéo (201) :

published https://webtv.example.com/recycler-la-fibre-de-carbone-trois-procedes-compares/

Le second retrouve l’asset par son external_ref et la publication par ?channel_id=…&asset_id=… : il n’envoie ni POST /v1/assets ni POST /v1/publications. Ses trois écritures — les tags, le lien, la publication — envoient ce que l’asset et la publication portent déjà, et n’écrivent rien : mêmes réponses, et aucun updated_at qui bouge. Il affiche la même ligne.

Deux choses à savoir avant d’en faire votre synchronisation :

  • Le script dit l’état voulu, et le rétablit : une catégorie ajoutée à la main dans Videas, ou une publication archivée depuis, est défaite au passage suivant. Ne lui faites envoyer que les champs dont votre source de données est la référence.
  • Le POST /v1/assets d’une ligne déjà créée n’est jamais renvoyé, parce que la recherche par external_ref le précède. Sans elle, une relance créerait un second asset — Relancer sans risque.

Erreurs

Toutes les erreurs ont le même corps — {"code": "…", "message": "…", "request_id": "…"}. Testez code ; message s’adresse à un humain et peut changer.

Statut code Ce qui s’est passé
400 validation Création ou modification : status: archived à la création, ou un published_at à côté d’un brouillon ; un published_at seul sur une publication qui n’est pas publiée ; un slug mal formé, trop long, ou qu’utilise le site de la chaîne lui-même ; un asset d’un type qu’une chaîne ne publie pas ; plus de 100 category_ids, une catégorie d’une autre chaîne, ou une catégorie principale qui n’est pas parmi elles ; un access autre que public ou authenticated ; null sur un champ qui ne peut pas être vidé. Lectures : un channel_id ou un asset_id qui n’est pas un seul uuid, un status inconnu, un paramètre inconnu. Le message nomme le champ.
401 unauthorized Pas de clé, ou une clé inconnue, expirée ou révoquée.
403 forbidden La clé ne porte pas websites.catalogue.read (lectures) ou websites.catalogue.publish (écritures) — ou l’organisation ne publie pas de chaînes.
404 not_found Aucune chaîne de GET /v1/channels, aucun asset de GET /v1/assets ou aucune publication de GET /v1/publications n’a cet identifiant — dans le chemin comme dans le corps.
409 conflict L’asset est déjà publié sur cette chaîne ; il n’est pas encore ready ; le slug est pris par une autre publication ou une page de la chaîne ; access ferait sortir une publication restricted, paid ou on_request ; ou les chaînes de l’organisation sont en cours de transfert vers Videas.
415 unsupported_media_type Le corps n’est pas envoyé en application/json.
422 validation channel_id ou asset_id manquant à la création, ou envoyé à une modification ; un champ mal typé ; un status ou un access qui ne nomme rien ; un champ que le corps ne connaît pas.
429 rate_limited Trop de requêtes : attendez le nombre de secondes indiqué par Retry-After.

Pour aller plus loin