You can attach media files such as images and documents to help center articles so that users can download them. You can attach files of various file formats. The total number of files must be within the knowledge base limits. The maximum size of an article attachment is 20 MB.

This article shows how to use the Guide Media Objects API to upload media files and attach them to help center articles.

Disclaimer: Zendesk provides this article for instructional purposes only. Zendesk doesn't provide support for the apps or example code in this tutorial. Zendesk doesn't support third-party technologies, such as Node.js, Python, or related libraries.

What you need

Uploading a media file to a help center article

Node.js

The following JavaScript example demonstrates how to use the Guide Medias API to:

  • Create the media upload URL.
  • Upload the media file.
  • Create the media entry.
  • Associate the media with an article.

Installing the dependencies

  1. In your project folder, use the following command to create an npm package.

    npm init -y
  2. Use the following command to install the dotenv and mime-types packages.

    npm install dotenv mime-types

    View package.json to verify these dependencies are installed.

  3. Create a .env file in your project folder to store your environment variables. The file should look like this:

    EMAIL={your-admin-email}TOKEN={your-api-token}SUBDOMAIN={your-zendesk-subdomain}FILENAME={relative-path-to-file-to-upload}ARTICLEID={your-article-id}ZENDESKDOMAIN=zendesk.com

    Replace the placeholders with your actual Zendesk account details and the path to the media file you want to upload.

    An example of the FILENAME might be media/image.png. The ARTICLEID is the id of the article to which the media file should be attached.

Creating the script

In your project folder, create a uploadMedia.js file and add the following code:

#!/usr/bin/node
require('dotenv').config();
const https = require('https');const fs = require('fs');const mime = require('mime-types');const querystring = require('querystring');
const email = process.env.EMAIL;const apiToken = process.env.TOKEN;const subdomain = process.env.SUBDOMAIN;const zendeskDomain = process.env.ZENDESKDOMAIN;const permissionGroupId = process.env.PERMISSIONGROUPID;const filename = process.env.FILENAME;const locale = process.env.LOCALE;const articleId = process.env.ARTICLEID; // New variable for article ID
// Updated the Authorization header to use API tokenconst headers = {    'Authorization': 'Basic ' + Buffer.from(`${email}/token:${apiToken}`).toString('base64'),    'Content-Type': 'application/json'};
function createMediaUrl() {    return new Promise((resolve, reject) => {        const filePath = __dirname + '/' + filename;        const contentType = mime.lookup(filePath);        const stats = fs.statSync(filePath);
        // Set up options for the media URL creation request        const mediaUrlCreateOptions = {            hostname: subdomain + '.' + zendeskDomain,            method: 'POST',            path: '/api/v2/guide/medias/upload_url',            headers: headers        };
        // Make the HTTPS request to create the media URL        const mediaUrlCreateRequest = https.request(mediaUrlCreateOptions, (res) => {            let data = [];            console.log('Media URL Creation Status Code:', res.statusCode);
            res.on('data', (chunk) => {                data.push(chunk);            });
            res.on('end', () => {                const response = JSON.parse(Buffer.concat(data).toString()); // Parse the response                resolve(response); // Resolve the promise with the response            });
            res.on('error', (error) => {                reject(error);            });        });
        // Send the request with the necessary data        const data = JSON.stringify({ "content_type": contentType, "file_size": stats.size });        mediaUrlCreateRequest.write(data);        mediaUrlCreateRequest.end();    });}
function uploadFile(uploadUrl) {    return new Promise((resolve, reject) => {        const url = new URL(uploadUrl.url); // Parse the upload URL        const path = url.pathname + url.search; // Get the path and query string        const s3Headers = JSON.parse(uploadUrl.headers); // Parse the headers for the upload
        // Set options for the file lupload request        const mediaUploadOptions = {            hostname: url.host,            method: 'PUT',            path: path,            headers: s3Headers        };
        // Make the HTTPS request to upload the file        const mediaUploadRequest = https.request(mediaUploadOptions, (res) => {            console.log('File Upload Status Code:', res.statusCode);            let data = [];
            res.on('data', (chunk) => {                data.push(chunk); // Collect response data            });
            res.on('end', () => {                const response = Buffer.concat(data).toString(); // Concatenate the response                resolve(response); //  Resolve the promise with the response            });
            res.on('error', (error) => {                reject(error);            });        });
        // Read the file and send it in the request        const file = fs.readFileSync(__dirname + '/' + filename);        mediaUploadRequest.end(file, 'binary');    });}
function createMedia(uploadUrl) {    return new Promise((resolve, reject) => {        const mediaCreateOptions = {            hostname: subdomain + '.' + zendeskDomain,            method: 'POST',            path: '/api/v2/guide/medias',            headers: headers        };
        // Make the HTTPS request to create the media        const mediaCreateRequest = https.request(mediaCreateOptions, (res) => {            let data = [];            console.log('Media Creation Status Code:', res.statusCode);
            res.on('data', (chunk) => {                data.push(chunk); // Collect response data            });
            res.on('end', () => {                const response = JSON.parse(Buffer.concat(data).toString()); // Parse the response                resolve(response);            });
            res.on('error', (error) => {                reject(error);            });        });
        // Send the request with the necessary data        const data = JSON.stringify({ "asset_upload_id": uploadUrl.asset_upload_id, "filename": filename });        mediaCreateRequest.write(data);        mediaCreateRequest.end();    });}
function associateMediaWithArticle(mediaId) {    return new Promise((resolve, reject) => {        // Set options for the media association request        const mediaAssociationOptions = {            hostname: subdomain + '.' + zendeskDomain,            method: 'POST',            path: `/api/v2/help_center/articles/${articleId}/attachments.json`, // Ensure this path is correct            headers: {                'Authorization': 'Basic ' + Buffer.from(`${email}/token:${apiToken}`).toString('base64'),                'Content-Type': 'application/x-www-form-urlencoded' // Set Content-Type for form data            }        };
        // Make the HTTPS request to associate the media        const mediaAssociationRequest = https.request(mediaAssociationOptions, (res) => {            let data = [];            console.log('Media Association Status Code:', res.statusCode);
            res.on('data', (chunk) => {                data.push(chunk); // Collect response data            });
            res.on('end', () => {                const response = JSON.parse(Buffer.concat(data).toString()); // Parse the response                resolve(response);            });
            res.on('error', (error) => {                reject(error);            });        });
        // Log the media ID being sent for association        console.log('Associating Media ID:', mediaId);
        // Ensure the payload is structured correctly        const formData = querystring.stringify({            "inline": "false",            "guide_media_id": mediaId // Use guide_media_id as per the Zendesk API        });
        mediaAssociationRequest.write(formData); // Send for data        mediaAssociationRequest.end();    });}
async function executeMediaUploadAndAssociation() {    try {        const uploadUrl = await createMediaUrl(); //  Create media upload URL        console.log('Upload URL Response:', uploadUrl);        const mediaUpload = await uploadFile(uploadUrl.upload_url); // Upload the file        console.log('File Upload Response:', mediaUpload);        const mediaCreate = await createMedia(uploadUrl.upload_url); // Create media entry        console.log('Media Creation Response:', mediaCreate);
        // Check if media was created successfully        if (!mediaCreate.media || !mediaCreate.media.id) {            throw new Error('Media creation failed. No media ID returned.');        }
        // Associate the media with the article        const mediaId = mediaCreate.media.id; // Extract media ID from the response        const associationResponse = await associateMediaWithArticle(mediaId);        console.log('Media associated with article:', associationResponse);    } catch (error) {        console.error('Error during media creation:', error); // Log any errors    }}
// Start the media upload and association processexecuteMediaUploadAndAssociation();

How it works

The script does the following:

  1. Imports the required modules to handle HTTP requests, file system operations, MIME type detection, and URL query string formatting. Loads environment variables from a .env file using the dotenv package.

    require("dotenv").config()
    const https = require("https")const fs = require("fs")const mime = require("mime-types")const querystring = require("querystring")
    const email = process.env.EMAILconst apiToken = process.env.TOKENconst subdomain = process.env.SUBDOMAINconst zendeskDomain = process.env.ZENDESKDOMAINconst permissionGroupId = process.env.PERMISSIONGROUPIDconst filename = process.env.FILENAMEconst locale = process.env.LOCALEconst articleId = process.env.ARTICLEID
  2. Constructs an authorization header using your Zendesk email and API token.

    const headers = {  Authorization:    "Basic " + Buffer.from(`${email}/token:${apiToken}`).toString("base64"),  "Content-Type": "application/json"}
  3. Creates the medial upload URL.

    The createMediaUrl function sends a POST request to generate a temporary URL for uploading the media file. It constructs the request with the necessary headers and payload, which includes the content type and file size of the media file.

  4. Uploads the media file.

    The uploadFile function takes the upload URL and sends a PUT request to that URL to upload the media file. It reads the file from the local file system and sends it in the request body.

  5. Creates the media in Zendesk.

    The createMedia function sends a POST request to Zendesk to create a new medial entry using the upload URL. It includes the asset upload ID and the filename in the request payload.

  6. Associates media with an article.

    The associateMediaWithArticle function sends a POST request to associate the upload media with a specific article in Zendesk. It uses the media ID from the previous response and sends it as part of the form data in the request.

  7. Runs the upload and association process.

    The executeMediaUploadAndAssociation function runs the entire process by calling the functions in sequence. If any step fails, it catches the error and logs it to the console.

Running the script

To run the script, type the following:

node uploadMedia.js

Example output

Media URL Creation Status Code: 200Upload URL Response: {  upload_url: {    asset_upload_id: '01JKF2ANHT267RGPH6JK7VMWFD',    url: 'https://uploaded-assets-pod23.s3.us-east-1.amazonaws.com/23/10992015/01JKF2ANHT267RGPH6JK7VMWFD?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIARVNLH63KNKT7BFPC%2F20250207%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250207T020256Z&X-Amz-Expires=3600&X-Amz-Signature=5c092ab85bcd053406047d16523a88cd1676bd15c11ac825a07d6b29795155de&X-Amz-SignedHeaders=content-disposition%3Bhost%3Bx-amz-server-side-encryption&x-id=PutObject',    headers: '{"Content-Disposition":"attachment; filename=\\"01JKF2ANHT267RGPH6JK7VMWFD.png\\"","Content-Type":"image/png","X-Amz-Server-Side-Encryption":"AES256"}'  }}File Upload Status Code: 200File Upload Response:Media Creation Status Code: 200Media Creation Response: {  media: {    id: '01JKF0N93E4K7JKTEEDCPTHZG7',    access_key: '01JKF0NAR81SQHTBV4C5DMRDHE',    name: 'test2.png',    size: 110612,    url: '/guide-media/01JKF0NAR81SQHTBV4C5DMRDHE',    content_type: 'image/png',    version: 0,    created_at: '2025-02-07T01:33:48Z',    updated_at: '2025-02-07T01:33:48Z'  }}Associating Media ID: 01JKF0N93E4K7JKTEEDCPTHZG7Media Association Status Code: 201Media associated with article: {  article_attachment: {    id: 29818683082903,    url: 'https://z3n1234.zendesk.com/api/v2/help_center/articles/attachments/29818683082903',    article_id: 28574842865175,    display_file_name: 'test2.png',    file_name: 'test2.png',    locale: null,    content_url: 'https://z3n1234.zendesk.com/hc/article_attachments/29818683082903',    relative_path: '/hc/article_attachments/29818683082903',    content_type: 'image/png',    size: 110612,    inline: true,    created_at: '2025-02-07T02:02:58Z',    updated_at: '2025-02-07T02:02:58Z'  }}

Use List Article Attachments to verify the article's attachments. In this example, test2.png is attached to the article with the id 28574842865175.

{  "article_attachments": [    {      "id": 29818683082903,      "url": "https://z3n1234.zendesk.com/api/v2/help_center/articles/attachments/29818683082903",      "article_id": 28574842865175,      "display_file_name": "test2.png",      "file_name": "test2.png",      "locale": null,      "content_url": "https://z3n1234.zendesk.com/hc/article_attachments/29818683082903",      "relative_path": "/hc/article_attachments/29818683082903",      "content_type": "image/png",      "size": 110612,      "inline": true,      "created_at": "2025-02-07T02:02:58Z",      "updated_at": "2025-02-07T02:02:58Z"    }  ]}

Python

The following Python script achieves the same functionality as the JavaScript code above.

import osimport jsonfrom pathlib import Pathimport mimetypesimport requests"""Install the requests library (https://requests.readthedocs.io/en/latest/user/install/).Provide values for the placeholders in the constants below.To run, navigate to this file in Terminal and run `python upload_media.py`."""SUBDOMAIN = '{your-zendesk-subdomain}'ROOT = f'https://{SUBDOMAIN}.zendesk.com'EMAIL = '{your-admin-email}'API_TOKEN = '{your-api-token}'AUTH = f'{EMAIL}/token', API_TOKENARTICLE_ID = '{your-article-id}'FILE_NAME = 'file-name'FILE_PATH = '{relative-path-to-file-to-upload}'
def create_upload_url_data() -> dict:    url = f'{ROOT}/api/v2/guide/medias/upload_url'    file_path = Path(FILE_PATH, FILE_NAME)    content_type = mimetypes.guess_type(file_path)[0]    file_size = os.path.getsize(file_path)    data = {'content_type': content_type,  'file_size': file_size}    response = requests.post(url, json=data, auth=AUTH)    if response.status_code != 200:        raise RuntimeError(f'{response.status_code}: {response.text}')    upload_url_data = response.json()    return upload_url_data
def upload_file(upload_url_data: dict) -> None:    url = upload_url_data['upload_url']['url']    headers: dict = json.loads(upload_url_data['upload_url']['headers'])    file_path = Path(FILE_PATH, FILE_NAME)    files = {'file': file_path.open('rb')}    response = requests.put(url, headers=headers, files=files)  # s3 expects a PUT request for uploading objects    files['file'].close()    if response.status_code != 200:        raise RuntimeError(f'{response.status_code}: {response.text}')    print(f'Successfully uploaded file {FILE_NAME}')
def create_media_object(upload_url_data: dict) -> dict:    url = f'{ROOT}/api/v2/guide/medias'    data = {        'asset_upload_id': upload_url_data['upload_url']['asset_upload_id'],        'filename': FILE_NAME    }    response = requests.post(url, json=data, auth=AUTH)    if response.status_code != 200:        raise RuntimeError(f'{response.status_code}: {response.text}')    print(f'Successfully created media object')    media_object = response.json()    return media_object
def add_attachment(media_object) -> None:    guide_media_id = media_object['media']['id']    url = f'{ROOT}/api/v2/help_center/articles/{ARTICLE_ID}/attachments'    form_data = {        'inline': 'false',        'guide_media_id': guide_media_id    }    response = requests.post(url, data=form_data, auth=AUTH)    if response.status_code != 201:        error = f'{response.status_code}: {response.text}'        raise RuntimeError(error)    print(f'Successfully added attachment to article {ARTICLE_ID}')
def main():    upload_url_data = create_upload_url_data()    upload_file(upload_url_data)    media_object = create_media_object(upload_url_data)    add_attachment(media_object)
if __name__ == "__main__":    main()

Troubleshooting

If the article does not have the expected attachment, check the returned error message. Common status codes are:

  • 200: Success
  • 400: Bad Request: Indicates an issue with the request payload
  • 401: Unauthorized: Indicates an issue with authentication
  • 404: Not Found: Indicates an issue with the URLs
  • 500: Internal Server Error: Indicates a server-side issue