Destination Youtube: Sync WooCommerce Products & Youtube Videos


This is not a tutorial, but more like sharing a nice geeky road-trip ;)

I have a pretty good understanding of the Youtube Data API, as I have actively used it on portalZINE TV in the past, to upload videos and dynamically link them to my local post-types.

For one of my latest customer projects (TYPEMYKNIFE /, the task was a bit more complicated and the goal was to make it as future-proof as it can be with the Google APIs :)

Prerequisites / References to get you started:


The goal for the setup was to actively synchronize WooCommerce products with linked / attached videos, with their source at Youtube.

As the website is multilingual, WPML integration is critical as well. And as Youtube allows localization of title and description, that can be added into the mix quiet easily in the future ;)

The following product attributes should be mirrored and optimised for Youtube:

  • Product Title
  • Product Description (5000 character limit at Youtube)
    My customer already has a pretty long and detailed description, which is perfect for Youtube!
    We average around 2900-3000 characters.
  • Product Tags (500 character length limit at Youtube)

The following attributes should be integrated into the description to enrich the Youtube description:

  • Introduction
  • Product Link to WooCommerce Product / Shop
  • Socials
  • Legal Information
  • Hash Tags (local post-type to add recurring / important hash tags)
  • Outro

All of these attributes will be collected internally and assigned using a simple template system, which allows the customer to move parts around freely and freely layout the description for Youtube.

The following stats will be collected for review:

  • Products without a video linked (no relation)
  • Products with the same video linked (duplication)
  • Videos without a product in the system
  • Total amount of videos / amount of video-pages (50 videos max per page)
  • Total amount of products with videos

Youtube SEO

These are the relevant key aspects, that help to get your videos more views.

  • Relevant text featuring at least 1,000 characters
  • Keywords that are relevant on Google Search and YouTube and describe your video
  • Hashtags, can also be used within the text
  • Timestamps Links to related content / affiliate links


In the past access to the Youtube Data API was far easier and less limited, when it comes to offline / none expiring OAuth2 refresh tokens.

When you are building a server-side application that is only available to your customer or moderators, it makes no sense to run that app through the Google App verification. Your app will never be used in public.

The Youtube Data API and its scopes, are defined as sensitive and therefor require third-party security assessment for public access.

The scopes I am requesting are +

Because of that its far easier to just setup OAuth 2 in test mode and restrict access to your customer and specific additional accounts only (up to 100 test users allowed). What all these account need, is access to your own or Brand Youtube Channel.

Preparation in the Google Cloud Console:

  • Activate Youtube Data API
  • Create OAuth2 Client ID
    • Name
    • Allowed Redirects
  • Setup OAuth-Consent Screen
    • Name of the App
    • Support Email
    • Logo
    • Allowed Domains
    • Typ: External
    • Setup all test-users, all of these need access to your Youtube channel already

A detailed description can be found here.

You can circumvent verification for the consent screen, by using an organisation setup at Google. Here some infos about that. With that setup offline refresh tokens should work fine.

Update: Just tried that, but wont work with a branded youtube account, even though the cloud user has admin access to it. Not giving up yet, but Google / Youtube really makes it difficult to just have a simple offline solution for specific tasks ;) BTW also forced the login hint, to make sure the right account is logged in : $client->setLoginHint(‘YourWoreksapceAccount’); !

You might have heard of the “The League of Extraordinary Packages“. It is a group of developers who have banded together to build solid, well tested PHP packages using modern coding standards.

They also offer an OAuth2-client + OAuth2 Google extension that can be used.


On the server, the Google API PHP SDK can be easily integrated using Composer.

In my customer plugin I neatly separated all relevant areas in classes & traits:

  • Online Authentification, with a 60 minute session
  • Offline Authentification, with a 1 week expiration (might explain that in more detail in the future.)
  • Error handling and debug information
  • Listing of videos
  • Updating of videos
  • Updating of tags
  • Updating hashtags
  • Backup old data locally, before update. There should always be an option to restore, especially with 500 videos :)
  • Settings & templates
  • REST routes / endpoints

You can check the expiry time of your access token by accessing:

“A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of “Testing” is issued a refresh token expiring in 7 days.” – Google

Basic Auth example from the SDK:

  1. <?php
  3. // Call set_include_path() as needed to point to your client library.
  4. set_include_path($_SERVER['DOCUMENT_ROOT'] . '/directory/to/google/api/');
  5. require_once 'Google/Client.php';
  6. require_once 'Google/Service/YouTube.php';
  8. /*
  9.  * You can acquire an OAuth 2.0 client ID and client secret from the
  10.  * {{ Google Cloud Console }} <{{ }}>
  11.  * For more information about using OAuth 2.0 to access Google APIs, please see:
  12.  * <>
  13.  * Please ensure that you have enabled the YouTube Data API for your project.
  14.  */
  15. $OAUTH2_CLIENT_ID = '';
  17. $REDIRECT = 'http://localhost/oauth2callback.php';
  21. $client = new Google_Client();
  22. $client->setClientId($OAUTH2_CLIENT_ID);
  23. $client->setClientSecret($OAUTH2_CLIENT_SECRET);
  24. $client->setScopes('');
  25. $client->setRedirectUri($REDIRECT);
  26. $client->setApplicationName($APPNAME);
  27. $client->setAccessType('offline');
  30. // Define an object that will be used to make all API requests.
  31. $youtube = new Google_Service_YouTube($client);
  33. if (isset($_GET['code'])) {
  34.     if (strval($_SESSION['state']) !== strval($_GET['state'])) {
  35.         die('The session state did not match.');
  36.     }
  38.     $client->authenticate($_GET['code']);
  39.     $_SESSION['token'] = $client->getAccessToken();
  41. }
  43. if (isset($_SESSION['token'])) {
  44.     $client->setAccessToken($_SESSION['token']);
  45.     echo '<code>' . $_SESSION['token'] . '</code>';
  46. }
  48. // Check to ensure that the access token was successfully acquired.
  49. if ($client->getAccessToken()) {
  50.     try {
  51.         // Call the channels.list method to retrieve information about the
  52.         // currently authenticated user's channel.
  53.         $channelsResponse = $youtube->channels->listChannels('contentDetails', array(
  54.             'mine' => 'true',
  55.         ));
  57.         $htmlBody = '';
  58.         foreach ($channelsResponse['items'] as $channel) {
  59.             // Extract the unique playlist ID that identifies the list of videos
  60.             // uploaded to the channel, and then call the playlistItems.list method
  61.             // to retrieve that list.
  62.             $uploadsListId = $channel['contentDetails']['relatedPlaylists']['uploads'];
  64.             $playlistItemsResponse = $youtube->playlistItems->listPlaylistItems('snippet', array(
  65.                 'playlistId' => $uploadsListId,
  66.                 'maxResults' => 50
  67.             ));
  69.             $htmlBody .= "<h3>Videos in list $uploadsListId</h3><ul>";
  70.             foreach ($playlistItemsResponse['items'] as $playlistItem) {
  71.                 $htmlBody .= sprintf('<li>%s (%s)</li>', $playlistItem['snippet']['title'],
  72.                     $playlistItem['snippet']['resourceId']['videoId']);
  73.             }
  74.             $htmlBody .= '</ul>';
  75.         }
  76.     } catch (Google_ServiceException $e) {
  77.         $htmlBody .= sprintf('<p>A service error occurred: <code>%s</code></p>',
  78.             htmlspecialchars($e->getMessage()));
  79.     } catch (Google_Exception $e) {
  80.         $htmlBody .= sprintf('<p>An client error occurred: <code>%s</code></p>',
  81.             htmlspecialchars($e->getMessage()));
  82.     }
  84.     $_SESSION['token'] = $client->getAccessToken();
  85. } else {
  86.     $state = mt_rand();
  87.     $client->setState($state);
  88.     $_SESSION['state'] = $state;
  90.     $authUrl = $client->createAuthUrl();
  91.     $htmlBody = <<<END
  92.   <h3>Authorization Required</h3>
  93.   <p>You need to <a href="$authUrl">authorise access</a> before proceeding.<p>
  94. END;
  95. }
  96. ?>
  98. <!doctype html>
  99. <html>
  100. <head>
  101.     <title>My Uploads</title>
  102. </head>
  103. <body>
  104. <?php echo $htmlBody?>
  105. </body>
  106. </html>

A simple upload example can be found here .

  1.  try{
  3.     // REPLACE this value with the video ID of the video being updated.
  4.     $videoId = "VIDEO_ID";
  6.     // Call the API's videos.list method to retrieve the video resource.
  7.     $listResponse = $youtube->videos->listVideos("snippet",
  8.         array('id' => $videoId));
  10.     // If $listResponse is empty, the specified video was not found.
  11.     if (empty($listResponse)) {
  12.       $htmlBody .= sprintf('<h3>Can\'t find a video with video id: %s</h3>', $videoId);
  13.     } else {
  14.       // Since the request specified a video ID, the response only
  15.       // contains one video resource.
  16.       $video = $listResponse[0];
  17.       $videoSnippet = $video['snippet'];
  18.       $tags = $videoSnippet['tags'];
  20.       // Preserve any tags already associated with the video. If the video does
  21.       // not have any tags, create a new list. Replace the values "tag1" and
  22.       // "tag2" with the new tags you want to associate with the video.
  23.       if (is_null($tags)) {
  24.         $tags = array("tag1", "tag2");
  25.       } else {
  26.         array_push($tags, "tag1", "tag2");
  27.       }
  29.       // Set the tags array for the video snippet
  30.       $videoSnippet['tags'] = $tags;
  32.       // Update the video resource by calling the videos.update() method.
  33.       $updateResponse = $youtube->videos->update("snippet", $video);
  35.       $responseTags = $updateResponse['snippet']['tags'];
  37.       $htmlBody .= "<h3>Video Updated</h3><ul>";
  38.       $htmlBody .= sprintf('<li>Tags "%s" and "%s" added for video %s (%s) </li>',
  39.           array_pop($responseTags), array_pop($responseTags),
  40.           $videoId, $video['snippet']['title']);
  42.       $htmlBody .= '</ul>';
  43.     }
  44.   } catch (Google_Service_Exception $e) {
  45.     $htmlBody .= sprintf('<p>A service error occurred: <code>%s</code></p>',
  46.         htmlspecialchars($e->getMessage()));
  47.   } catch (Google_Exception $e) {
  48.     $htmlBody .= sprintf('<p>An client error occurred: <code>%s</code></p>',
  49.         htmlspecialchars($e->getMessage()));
  50.   }


All operations to and from the Youtube Data API are rate limited. What is important for us, are the queries per day.

The default quota is 10.000 queries per day, sounds a lot, but is easily gone after updating 150-200 videos. You can request this limit to be raised, but again a lot of paperwork and questions that are just not needed.

The above limit just means, that you need to cache as many queries as possible, to only query live when needed ;)

Something you learn fast, when experimenting with different things! I hit that limit multiple times in the first few days, with around 500 videos in the queue.

Different operation cost you different amount of units

  • A read operation that retrieves a list of resources — channels, videos, playlists — usually costs 1 unit.
  • A write operation that creates, updates, or deletes a resource usually costs 50 units.
    10.000 / 50 are 200 updates per day :)
  • A search request costs 100 units.
  • A video upload costs 1600 units. Glad we are not handling the upload as well, but technically easy ;)

It also helps to use the Google Developer Playground to testdrive the Youtube Data API with your own credentials while optimising your own code.
You can define your own OAuth 2.0 configuration by clicking the cog in the upper right corner.

I setup the bulk updating to allow splitting it over multiple days, if required. For this an offline refresh token is needed, as the standard token expires after 60 minutes.

My customer can also just update a single video, when changes are applied to the product or a new product has been added.

If more frequent updates are required, I will ask for a raise of the queries per day. You can circumvent the limit by using multiple Google Cloud Platform accounts with new OAuth credentials, but really an overkill right now. I have done that in the past ;)


The GUI is just based of Bootstrap, to make it simple and clean. Using my own wrapper to make it work within the WordPress admin.

For all ajax operations, I am using htmx and _hyperscript, which I will talk about in another article in the future.

Really neat and clean way to build single page interfaces.

The whole plugin runs of its own REST API endpoint. Just love using WordPress as a headless system.

I used TWIG / Timber for the templates, to separate logic and layout. Timber has been my goto solution for years now. It drives my own and many customer websites.


This has been a lot of fun, maybe a bit too much LOL

I do geek-out about many of my projects, but this experience helped me to bring my WordPress toolbox to the next level. This will help to drive other things in the future.

Working so deeply with the Youtube Data API has been fun and feels so easy now, after all remaining problems have been solved.

Would have loved this during my portalZINE TV days ;)

I you read all this, you just earned yourself a badge for completion ;)

Need something similar or something else? Just say hi and we can talk.



I am a full-stack developer.

I love programming,  design and know my way around server architecture as well.  I would never feel complete, with one of these missing.

I have a broad range of interests, that’s why I constantly dive into new technologies and expand my knowledge where ever required. Technologies are evolving fast and I enjoy using the latest.

Apart from that, I am a peace loving guy who tries to have people around him that think the same.  I truly believe in the principle: “If you help someone, someone will help you, when you need it."

Recent Posts

3D Messer Gravur Konfigurator – TYPEMYKNIFE® | 2022

TYPEMYKNIFE® - allows you to laser ingrave your knife using a unique 3D configurator. I redesigned their 3D knife configurator… Read More

2 days ago

Display Calibration / Spyder / Mac M1

Why? Color correction is important to me and to the projects I do. Regularly calibrating your monitor is essential in… Read More

2 months ago

Hosting MP3 files on Google Drive + HTML5 Audio Player

What Google Drive Sharing offers us! I am doing a Podcast on portalZINE.TV since last year and always host a… Read More

3 months ago

Importing CSV files with HTML5 and Papa Parse!

PAPA PARSE Papa Parse is a powerful, in-browser CSV parser for the big boys and girls :)If you do need… Read More

3 months ago

5 auf einen Streich – Folge 104

Die erste Folge nach einer langen Pause ;) Folge 104 Read More

3 months ago

Podcast – 5 auf einen Streich

UMZUG Moin! Im Juni hatte ich den Umbau von portalZINE TV angekündigt und kündige jetzt den Umzug des Podcasts -… Read More

3 months ago