Attaching media files to help center articles with the Zendesk API
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
-
A Zendesk Guide plan with Guide admin access. See About the Zendesk Guide plan types in Zendesk help.
-
Zendesk help center enabled and activated.
-
One of the following environments or programming languages:
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
-
In your project folder, use the following command to create an npm package.
npm init -y -
Use the following command to install the
dotenvandmime-typespackages.npm install dotenv mime-typesView
package.jsonto verify these dependencies are installed. -
Create a
.envfile 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.comReplace 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/noderequire('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 requestconst mediaUrlCreateOptions = {hostname: subdomain + '.' + zendeskDomain,method: 'POST',path: '/api/v2/guide/medias/upload_url',headers: headers};// Make the HTTPS request to create the media URLconst 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 responseresolve(response); // Resolve the promise with the response});res.on('error', (error) => {reject(error);});});// Send the request with the necessary dataconst 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 URLconst path = url.pathname + url.search; // Get the path and query stringconst s3Headers = JSON.parse(uploadUrl.headers); // Parse the headers for the upload// Set options for the file lupload requestconst mediaUploadOptions = {hostname: url.host,method: 'PUT',path: path,headers: s3Headers};// Make the HTTPS request to upload the fileconst 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 responseresolve(response); // Resolve the promise with the response});res.on('error', (error) => {reject(error);});});// Read the file and send it in the requestconst 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 mediaconst 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 responseresolve(response);});res.on('error', (error) => {reject(error);});});// Send the request with the necessary dataconst 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 requestconst mediaAssociationOptions = {hostname: subdomain + '.' + zendeskDomain,method: 'POST',path: `/api/v2/help_center/articles/${articleId}/attachments.json`, // Ensure this path is correctheaders: {'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 mediaconst 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 responseresolve(response);});res.on('error', (error) => {reject(error);});});// Log the media ID being sent for associationconsole.log('Associating Media ID:', mediaId);// Ensure the payload is structured correctlyconst formData = querystring.stringify({"inline": "false","guide_media_id": mediaId // Use guide_media_id as per the Zendesk API});mediaAssociationRequest.write(formData); // Send for datamediaAssociationRequest.end();});}async function executeMediaUploadAndAssociation() {try {const uploadUrl = await createMediaUrl(); // Create media upload URLconsole.log('Upload URL Response:', uploadUrl);const mediaUpload = await uploadFile(uploadUrl.upload_url); // Upload the fileconsole.log('File Upload Response:', mediaUpload);const mediaCreate = await createMedia(uploadUrl.upload_url); // Create media entryconsole.log('Media Creation Response:', mediaCreate);// Check if media was created successfullyif (!mediaCreate.media || !mediaCreate.media.id) {throw new Error('Media creation failed. No media ID returned.');}// Associate the media with the articleconst mediaId = mediaCreate.media.id; // Extract media ID from the responseconst 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:
-
Imports the required modules to handle HTTP requests, file system operations, MIME type detection, and URL query string formatting. Loads environment variables from a
.envfile using thedotenvpackage.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 -
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"} -
Creates the medial upload URL.
The
createMediaUrlfunction 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. -
Uploads the media file.
The
uploadFilefunction 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. -
Creates the media in Zendesk.
The
createMediafunction 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. -
Associates media with an article.
The
associateMediaWithArticlefunction 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. -
Runs the upload and association process.
The
executeMediaUploadAndAssociationfunction 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_datadef 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 objectsfiles['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_objectdef 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: Success400: Bad Request: Indicates an issue with the request payload401: Unauthorized: Indicates an issue with authentication404: Not Found: Indicates an issue with the URLs500: Internal Server Error: Indicates a server-side issue