Upload multiple videos via URL

Your JWP dashboard lets you manually upload single videos or retrieve up to 100 videos via URLs. However, suppose you need to add more than 100 videos or need to add videos programmatically to your media library. In that case, you can code a solution instructing JWP to fetch videos from URLs.


You can also manually upload videos from your JWP dashboard.


CSV file

The .csv file lists videos with relevant video metadata you want to add to your JWP library.

When creating your .csv file, a populated download_url column is required. We recommend including download_url, title, description, publish_start_date, and permalink as the first six column headers. You can include additional header columns to include custom video metadata.

Recommended and custom column headers

Recommended and custom column headers

The table below defines all possible column headers and the acceptable data format for each.


Special characters should not be HTML-encoded. HTML-encoded characters in the title, description, and tags columns should be decoded before pushing to the API.

Column Description
download_url* (Required) Remote URL of your video files

Be sure that your videos adhere to the guidelines here to ensure our platform can transcode them.
title Title of the video
description Description of the video
tags Tags for the video

Multiple tags should be comma-separated within quotation marks.
publish_start_date Video publish date in YYYY-MM-DD format

This specifies the date and time when the video should be available for streaming.
permalink URL of the web page where this video is published
external_id Unique identifier value in metadata on the media

For example, a foreign ID to a different system.

NOTE: This must be unique per media.
thumbnail_download_url URL of the video thumbnail location

If the URL is valid the upload script attempts to create a thumbnail for the media based on what is served at the URL, and sets that new thumbnail to be the media’s poster.
custom.legacy_id ID for the media, generated by a non-JW Player platform or program

For example, a foreign ID to a different system.

NOTE: A maximum of 64 characters is permitted. Unlike external_id, this value does not have a requirement to be unique per media item.
custom.param User-defined parameter

The param part of the column header -- after the period separator (.) -- specifies the parameter name.

A parameter name should adhere to the following naming rules:

  • Can contain letters, numbers, periods (.), underscores (_), and hyphens (-)
  • Cannot start with a number or punctuation character
  • Cannot contain spaces
  • Cannot use reserved names, such as:
    • feedid
    • file
    • mediaid
    • sources
    • tracks
  • Cannot use any existing non-custom parameter names, such as:
    • author
    • description
    • title
http://video.my_platform.net/2324731/5288693/16179765?signature=f00d4fae14055dd97c35987c7c1e4aae30865cb8,This is Title One,This is Description One,tag1,2012-08-25,http://www.my_site/page1.html,5288693,http://images.my_platform.net/archive/images/10055001.jpg,legacy id value,football
http://video.my_platform.net/2324731/5288693/16179766?signature=f00d4fae14055dd97c35987c7c1e4aae30865cb9,This is Title Two,This is Description Two,"tag1,tag2",2013-08-19,http://www.my_site/page2.html,5287268,http://images.my_platform.net/archive/images/10055002.jpg,legacy id value,tennis
http://video.my_platform.net/2324731/5288693/16179767?signature=f00d4fae14055dd97c35987c7c1e4aae30865cb0,This is Title Three,This is Description Three,tag2,2014-06-19,http://www.my_site/page3.html,5287267,http://images.my_platform.net/archive/images/10055003.jpg,legacy id value,cricket
http://video.my_platform.net/2324731/5288693/16179768?signature=f00d4fae14055dd97c35987c7c1e4aae30865cb1,This is Title Four,This is Description Four,"tag1,tag3",2014-08-19,http://www.my_site/page4.html,5287266,http://images.my_platform.net/archive/images/10055004.jpg,legacy id value,baseball

Add videos to your library

Use the following steps to upload the videos and associated information in the .csv file programmatically:

  1. Add the Python client library to your codebase.
  2. Add the .csv file to your codebase.
  3. Add the batch upload code to the same directory as the .csv file. Be sure to replace the placeholders: <site_id> and <api_secret>.


    This Python batch upload code performs the following tasks:

    • Imports the necessary code dependencies
    • Parses the .csv file you created into an array or list
    • Loops over each row to create a video record
    • Prints relevant JWP unique identifiers for resources created
import html
import csv
import requests
import time

from functools import wraps

SITE_ID = "<site_id>"
API_SECRET = "<api_secret>"
headers = {"Authorization": f"Bearer {API_SECRET}"}

timeout_wait_time = 2.0
max_retries = 6

def needs_unescaping(key):
    return key in ["title", "description", "tags"] or key.startswith("custom.")

def parse_row(row):
    media = {}
    custom_parameters = {}
    for key, value in row.items():
        if key.startswith('custom.'):
            custom_parameters[key.split('.')[1]] = value
        else :
            media[key] = html.unescape(value) if needs_unescaping(key) else value
    media['custom_params'] = custom_parameters
    return media

def retry(max_tries=6):
    def decorator_retry(func):

        def func_retry(*args, **kwargs):
            wait = timeout_wait_time
            func_response = func(*args, **kwargs)
            tries = 1
            status_code = func_response.status_code
            while status_code == 429 and tries <= max_tries:
                print(f"Timeout hit. About to sleep {wait} seconds!")
                func_response = func(*args, **kwargs)
                status_code = func_response.status_code
                tries += 1
                wait *= wait
            if (tries > max_tries):
                print(f"Failed timeout retries more than {max_tries} times.")
            return func_response
        return func_retry
    return decorator_retry

def create_media(media_fields, content):
    url = f"https://api.jwplayer.com/v2/sites/{SITE_ID}/media/"
    payload = {
        "upload": {
            # refer to MAPI v2 documentation for method options: https://docs.jwplayer.com/platform/reference/post_v2-sites-site-id-media
            "method": "fetch",
            "download_url": content
        "metadata": media_fields,
    create_media_response = requests.post(url=url, headers=headers, json=payload)
    return create_media_response

def create_thumbnail(media_id, thumbnail_url):
    url = f"https://api.jwplayer.com/v2/sites/{SITE_ID}/thumbnails/"
    payload = {
        # refer to MAPI v2 documentation for thumbnail endpoints: https://docs.jwplayer.com/platform/reference/post_v2-sites-site-id-thumbnails
        "relationships": {"media": [{"id": f"{media_id}"}]},
        "upload": {
            "source_type": "custom_upload",
            "method": "fetch",
            "thumbnail_type": "static",
            "download_url": f"{thumbnail_url}"
    create_thumbnail_response = requests.post(url=url, headers=headers, json=payload)
    return create_thumbnail_response

def poll_thumbnail_for_ready_status(thumbnail_id):
    url = f"https://api.jwplayer.com/v2/sites/{SITE_ID}/thumbnails/{thumbnail_id}"
    status = "processing"

    while status != "ready":
        get_thumbnail_response = requests.get(url=url, headers=headers)
        status = get_thumbnail_response.json().get("status")
        print(f"{thumbnail_id} not yet ready. Waiting for one second.")
        # Rather than use @retry here, we wait. That's because it doesn't make sense to poll without some regular wait.
def update_thumbnail(thumbnail_id):
    url = f"https://api.jwplayer.com/v2/sites/{SITE_ID}/thumbnails/{thumbnail_id}"
    payload = {
        # refer to MAPI v2 Documentation for updating thumbnails: https://docs.jwplayer.com/platform/reference/patch_v2-sites-site-id-thumbnails-thumbnail-id-
        "relationships": {
            "media": [{"is_poster": True}]
    update_thumbnail_response = requests.patch(url=url, headers=headers, json=payload)
    return update_thumbnail_response

if __name__ == "__main__":
    with open("example_to_load.csv") as csvfile:
        reader = csv.reader(csvfile)
        headings = next(reader)
        for row in reader:
            row = dict(zip(headings, row))
            video = parse_row(row)
            media_metadata = {
                    "title": video.get("title"),
            legacy_id = video.get('custom_params',{}).get('legacy_id')
            if legacy_id :
                print(f"Uploading media with legacy id {legacy_id}:")
            elif video.get('external_id'):
                print(f"Uploading media with external id {video.get('external_id')}:")
                print(f"Uploading media with title {video.get('title')}:")

            create_media_response = create_media(media_metadata,video.get("download_url"))

            errors = create_media_response.json().get('errors')
            media_id = create_media_response.json().get("id")
            if errors:
                print(f'Errors on Creating Media with title {video.get("title")}:')
                print('You will need to retry this upload.')
                print(f"Media can be accessed in JW Dashboard. site: {SITE_ID}, media: {media_id}")
                create_thumbnail_response = create_thumbnail(

                errors = create_thumbnail_response.json().get('errors')
                thumbnail_id = create_thumbnail_response.json().get("id")
                if errors:
                    print(f'Errors on Creating thumbnail with url {video.get("thumbnail_download_url")}:')
                    print('The default thumbnail will be created by JWP.')
                    update_thumbnail_reponse = update_thumbnail(thumbnail_id=thumbnail_id)
                    errors = update_thumbnail_reponse.json().get('errors')
                    if errors:
                        print(f'Errors on Updating thumbnail with thumbnail ID {thumbnail_id}:')
                        print(f'Thumbnail {thumbnail_id} is in JW, but it is not the poster for {media_id}.')