{"id":543,"date":"2025-10-26T22:43:41","date_gmt":"2025-10-26T22:43:41","guid":{"rendered":"https:\/\/ykim.synology.me\/wordpress\/?p=543"},"modified":"2025-10-26T23:14:10","modified_gmt":"2025-10-26T23:14:10","slug":"how-to-use-youtube-reporting-api","status":"publish","type":"post","link":"https:\/\/ykim.synology.me\/wordpress\/how-to-use-youtube-reporting-api-543\/","title":{"rendered":"How to use YouTube Reporting API"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The most important thing to understand about the YouTube Reporting API is that it&#8217;s <strong>asynchronous<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Unlike the Analytics API (where you ask a question and get an answer), the Reporting API works in a multi-step process:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>You:<\/strong> Ask YouTube to create a <strong>&#8220;report job&#8221;<\/strong> (e.g., &#8220;Please start generating a daily report of my video stats&#8221;).<\/li>\n\n\n\n<li><strong>YouTube:<\/strong> Says &#8220;Okay&#8221; and, over the next 24-48 hours, starts generating these reports as <strong>CSV files<\/strong>.<\/li>\n\n\n\n<li><strong>You:<\/strong> Periodically check back and ask, &#8220;Are any new reports from my job ready?&#8221;<\/li>\n\n\n\n<li><strong>YouTube:<\/strong> &#8220;Yes, here is the download URL for yesterday&#8217;s report.&#8221;<\/li>\n\n\n\n<li><strong>You:<\/strong> Download the CSV file and import it into your database or spreadsheet.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The code is split into two main parts: <strong>(1)<\/strong> Creating the job and <strong>(2)<\/strong> Downloading the reports.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udc0d Python Code Examples<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You&#8217;ll need the Google API client library and the authentication library.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npip install google-api-python-client google-auth-oauthlib requests\n\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">You must first have an OAuth 2.0 <code>client_secrets.json<\/code> file from your Google Cloud project with the YouTube Reporting API enabled.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Part 1: Creating a Reporting Job<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is a one-time setup. You run this script once to tell YouTube what report you want it to start generating.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Python<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nimport os\nimport google_auth_oauthlib.flow\nimport googleapiclient.discovery\nimport googleapiclient.errors\n\nSCOPES = &#x5B;&quot;https.www.googleapis.com\/auth\/yt-analytics.readonly&quot;]\nAPI_SERVICE_NAME = &quot;youtubereporting&quot;\nAPI_VERSION = &quot;v1&quot;\nCLIENT_SECRETS_FILE = &quot;client_secrets.json&quot; # Your downloaded credentials\n\ndef get_authenticated_service():\n    &quot;&quot;&quot;Authenticates the user and returns the YouTube Reporting API service.&quot;&quot;&quot;\n    flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(\n        CLIENT_SECRETS_FILE, SCOPES)\n    credentials = flow.run_local_server(port=0)\n    return googleapiclient.discovery.build(\n        API_SERVICE_NAME, API_VERSION, credentials=credentials)\n\ndef list_report_types(youtube_reporting):\n    &quot;&quot;&quot;Lists available report types.&quot;&quot;&quot;\n    print(&quot;Listing available report types...&quot;)\n    results = youtube_reporting.reportTypes().list().execute()\n    report_types = results.get(&quot;reportTypes&quot;, &#x5B;])\n    \n    if not report_types:\n        print(&quot;No report types found.&quot;)\n    else:\n        for report_type in report_types:\n            print(f&quot;  ID: {report_type&#x5B;&#039;id&#039;]}, Name: {report_type&#x5B;&#039;name&#039;]}&quot;)\n    return report_types\n\ndef create_reporting_job(youtube_reporting, report_type_id, job_name):\n    &quot;&quot;&quot;Creates a new reporting job.&quot;&quot;&quot;\n    print(f&quot;Creating job for report type &#039;{report_type_id}&#039;...&quot;)\n    reporting_job = {\n        &quot;reportTypeId&quot;: report_type_id,\n        &quot;name&quot;: job_name\n    }\n    \n    try:\n        job = youtube_reporting.jobs().create(body=reporting_job).execute()\n        print(&quot;Job created successfully:&quot;)\n        print(f&quot;  Job ID: {job&#x5B;&#039;id&#039;]}&quot;)\n        print(f&quot;  Report Type ID: {job&#x5B;&#039;reportTypeId&#039;]}&quot;)\n        print(f&quot;  Name: {job&#x5B;&#039;name&#039;]}&quot;)\n        print(f&quot;  Create Time: {job&#x5B;&#039;createTime&#039;]}&quot;)\n    except googleapiclient.errors.HttpError as e:\n        print(f&quot;An HTTP error {e.resp.status} occurred: {e.content}&quot;)\n\nif __name__ == &quot;__main__&quot;:\n    # Remove OAUTHLIB_INSECURE_TRANSPORT warning for local testing\n    os.environ&#x5B;&quot;OAUTHLIB_INSECURE_TRANSPORT&quot;] = &quot;1&quot;\n    \n    service = get_authenticated_service()\n    \n    # First, see what reports are available\n    list_report_types(service)\n    \n    # --- Example: Create a &quot;Basic Channel Video Stats&quot; job ---\n    # You would get this ID from the list_report_types() output.\n    # &#039;channel_basic_a2&#039; is a common one for daily video stats.\n    REPORT_ID_TO_CREATE = &quot;channel_basic_a2&quot;\n    JOB_NAME = &quot;Daily Video Stats Job&quot;\n    \n    create_reporting_job(service, REPORT_ID_TO_CREATE, JOB_NAME)\n\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\">Part 2: Downloading an Available Report<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You would run this script daily (or hourly) to check for and download new report files.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Python<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nimport os\nimport requests\nimport google_auth_oauthlib.flow\nimport googleapiclient.discovery\nfrom io import FileIO\n\n# (Use the same get_authenticated_service() function from Part 1)\n# ... (get_authenticated_service code here) ...\n\ndef list_jobs(youtube_reporting):\n    &quot;&quot;&quot;Lists all created reporting jobs.&quot;&quot;&quot;\n    print(&quot;Listing all reporting jobs...&quot;)\n    results = youtube_reporting.jobs().list().execute()\n    jobs = results.get(&quot;jobs&quot;, &#x5B;])\n    \n    if not jobs:\n        print(&quot;No jobs found.&quot;)\n    else:\n        for job in jobs:\n            print(f&quot;  Job ID: {job&#x5B;&#039;id&#039;]}, Name: {job&#x5B;&#039;name&#039;]}, Report Type: {job&#x5B;&#039;reportTypeId&#039;]}&quot;)\n    return jobs\n\ndef get_latest_report_url(youtube_reporting, job_id):\n    &quot;&quot;&quot;Gets the download URL for the most recent report from a job.&quot;&quot;&quot;\n    print(f&quot;Finding reports for job ID: {job_id}...&quot;)\n    results = youtube_reporting.jobs().reports().list(jobId=job_id).execute()\n    reports = results.get(&quot;reports&quot;, &#x5B;])\n    \n    if not reports:\n        print(&quot;No reports found for this job yet. (It can take 24-48 hours to generate the first one)&quot;)\n        return None\n        \n    # Reports are listed newest-first, so index 0 is the latest\n    latest_report = reports&#x5B;0]\n    print(f&quot;Found report: {latest_report&#x5B;&#039;id&#039;]}&quot;)\n    print(f&quot;  Covers period: {latest_report&#x5B;&#039;startTime&#039;]} to {latest_report&#x5B;&#039;endTime&#039;]}&quot;)\n    print(f&quot;  Download URL: {latest_report&#x5B;&#039;downloadUrl&#039;]}&quot;)\n    return latest_report&#x5B;&#039;downloadUrl&#039;]\n\ndef download_report_file(report_url, local_file_name):\n    &quot;&quot;&quot;Downloads the report file from the given URL.&quot;&quot;&quot;\n    print(f&quot;Downloading report to &#039;{local_file_name}&#039;...&quot;)\n    \n    # Note: The download URL requires the same OAuth credentials\n    # For simplicity, this example uses &#039;requests&#039;, but a real app\n    # would add the auth headers from the &#039;credentials&#039; object.\n    # A simple GET request often works for testing.\n    response = requests.get(report_url)\n    \n    if response.status_code == 200:\n        with open(local_file_name, &quot;wb&quot;) as f:\n            f.write(response.content)\n        print(&quot;Download complete.&quot;)\n    else:\n        print(f&quot;Download failed with status code: {response.status_code}&quot;)\n        print(response.text)\n\nif __name__ == &quot;__main__&quot;:\n    os.environ&#x5B;&quot;OAUTHLIB_INSECURE_TRANSPORT&quot;] = &quot;1&quot;\n    service = get_authenticated_service()\n    \n    # First, list your jobs to find the ID you want\n    jobs = list_jobs(service)\n    \n    if jobs:\n        # Use the ID of the job you created in Part 1\n        JOB_ID_TO_DOWNLOAD = jobs&#x5B;0]&#x5B;&#039;id&#039;] # Just using the first job for this example\n        \n        report_url = get_latest_report_url(service, JOB_ID_TO_DOWNLOAD)\n        \n        if report_url:\n            download_report_file(report_url, &quot;my_youtube_report.csv&quot;)\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udcca Typical Output (The CSV File)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The API calls themselves just return JSON about the <em>jobs<\/em> and <em>reports<\/em> (like the text you see in the console output). The <strong>real output<\/strong> is the <code>.csv<\/code> file you download.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is a sample of what the <strong><code>channel_basic_a2<\/code><\/strong> report (a common one) looks like:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\ucf54\ub4dc \uc2a4\ub2c8\ud3ab<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ndate,channel_id,video_id,views,red_views,comments,likes,dislikes,shares,estimated_minutes_watched,average_view_duration,average_view_percentage,subscribers_gained,subscribers_lost,videos_added_to_playlists,videos_removed_from_playlists,annotation_click_through_rate,annotation_close_rate\n2025-10-24,UCxxxxxxxxxxxxxxxxxA,VIDEO_ID_ONE,1025,150,5,25,1,3,2840,166,45.2,12,1,3,0,0,0\n2025-10-24,UCxxxxxxxxxxxxxxxxxA,VIDEO_ID_TWO,5034,780,42,150,8,15,19870,236,52.1,45,3,10,2,0,0\n2025-10-24,UCxxxxxxxxxxxxxxxxxA,VIDEO_ID_THREE,890,40,1,12,0,1,1500,101,30.9,3,0,0,1,0,0\n\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\"><strong>Key takeaways from the output:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>It&#8217;s a standard CSV file.<\/li>\n\n\n\n<li>Each row represents one <strong>video<\/strong> for one <strong>day<\/strong>.<\/li>\n\n\n\n<li>It&#8217;s incredibly detailed, providing metrics like <code>views<\/code>, <code>estimated_minutes_watched<\/code>, <code>subscribers_gained<\/code>, and <code>shares<\/code> all broken down by <code>video_id<\/code>.<\/li>\n\n\n\n<li>This format is designed to be loaded directly into a database (like Google BigQuery, PostgreSQL, etc.) for analysis.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/developers.google.com\/youtube\/reporting\/v1\/reference\/rest\/v1\/jobs\/create#python\" target=\"_blank\" rel=\"noopener\">https:\/\/developers.google.com\/youtube\/reporting\/v1\/reference\/rest\/v1\/jobs\/create#python<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n<div style='text-align:center' class='yasr-auto-insert-overall'><\/div><div style='text-align:center' class='yasr-auto-insert-visitor'><\/div>","protected":false},"excerpt":{"rendered":"<p>The most important thing to understand about the YouTube Reporting API is that it&#8217;s asynchronous. Unlike the Analytics API (where you ask a question and get an answer), the Reporting API works in a multi-step process: The code is split into two main parts: (1) Creating the job and (2) Downloading the reports. \ud83d\udc0d Python&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"_kadence_starter_templates_imported_post":false,"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","yasr_overall_rating":0,"yasr_post_is_review":"","yasr_auto_insert_disabled":"","yasr_review_type":"","fifu_image_url":"","fifu_image_alt":"","iawp_total_views":0,"footnotes":""},"categories":[55,10],"tags":[],"class_list":["post-543","post","type-post","status-publish","format-standard","hentry","category-application-slug","category-software-slug"],"yasr_visitor_votes":{"stars_attributes":{"read_only":false,"span_bottom":false},"number_of_votes":0,"sum_votes":0},"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/posts\/543","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/comments?post=543"}],"version-history":[{"count":4,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/posts\/543\/revisions"}],"predecessor-version":[{"id":556,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/posts\/543\/revisions\/556"}],"wp:attachment":[{"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/media?parent=543"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/categories?post=543"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ykim.synology.me\/wordpress\/wp-json\/wp\/v2\/tags?post=543"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}