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
dotenv
andmime-types
packages.npm install dotenv mime-types
View
package.json
to verify these dependencies are installed. -
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 token
const 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 process
executeMediaUploadAndAssociation();
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
.env
file using thedotenv
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.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
-
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
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. -
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. -
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. -
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. -
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: 200
Upload 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: 200
File Upload Response:
Media Creation Status Code: 200
Media 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: 01JKF0N93E4K7JKTEEDCPTHZG7
Media Association Status Code: 201
Media 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 os
import json
from pathlib import Path
import mimetypes
import 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_TOKEN
ARTICLE_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 payload401: Unauthorized
: Indicates an issue with authentication404: Not Found
: Indicates an issue with the URLs500: Internal Server Error
: Indicates a server-side issue