
Administrator
Options
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
3 hours ago
Check out other articles in the C3 Series
Most ServiceNow developers think of REST APIs as JSON-in, JSON-out operations. Or XML if you have a preference for verbosity.
When we built C3, we discovered that ServiceNow's REST capabilities go way beyond JSON - you can handle binary files, serve images directly, export CSV data, and basically work with any content type you need.
This isn't just theoretical. We processed thousands of images, served them securely through custom REST endpoints, and even created CSV exports for our printing vendor. Turns out ServiceNow's REST APIs are more flexible than I originally realized, and once you know how to work with data streams, it opens a whole new set of possibilities.
Let me walk you through the three main approaches we used and what we learned about handling non-JSON content in ServiceNow.
The Problem: When JSON Isn't Enough
Traditional ServiceNow REST APIs handle JSON beautifully, but C3 needed more:
- Image Storage: AI-generated avatars from Replicate AI came back as binary image URLs that needed to be stored as attachments
- Secure Image Serving: We needed fine-grained control over who could access images without modifying record ACLs
- Data Export: Our printing vendor needed a CSV export with image URLs for the professional card printing process
Each of these required working with binary data or alternative attachment style content types in ways that aren't covered in more typical ServiceNow REST API paths.
Technique 1: GET a Binary File and save as Attachment
The first challenge was storing images returned from Replicate AI. Their API gives you URLs to generated images, and you need to send a GET request to fetch the image and store those binary files in ServiceNow.
Here's how we handled it in our ReplicateAI script include:
saveImageAsAttachment: function ({ imageUrl, tableName, recordSysId, fieldOrFileName }) {
const request = new sn_ws.RESTMessageV2();
request.setHttpMethod('GET');
request.setEndpoint(imageUrl);
request.saveResponseBodyAsAttachment(tableName, recordSysId, fieldOrFileName);
const attachmentId = request.execute().getResponseAttachmentSysid();
return attachmentId;
}
What this does
The
saveResponseBodyAsAttachment()
method is pure magic for binary file handling:
- Makes the GET request to fetch the binary image data
- Automatically handles the binary stream without you having to deal with base64 encoding
- Creates the attachment record in the sys_attachment table
- Returns the attachment sys_id so you can reference it in your records
Why it's awesome
- No base64 overhead: Direct binary transfer is about 33-37% smaller than base64 encoding of the image
- One-line solution: Handles all the complexity of binary data transfer and attachment creation
- Automatic content type detection: ServiceNow figures out the MIME type from the file content
Technique 2: Serving a Binary File
The second challenge was serving images back to clients with some custom logic. ServiceNow's built-in attachment serving basically just has an ACL check with no other handling, but we needed more flexible control, for some automated 3rd party processing.
Here's our scripted REST API for serving images:
(function process(request, response) {
const parentId = request.queryParams.parentId;
const type = request.queryParams.type;
var attachmentGr = new GlideRecord('sys_attachment');
if (type == 'avatar_without_bg') {
let qc = attachmentGr.addQuery('table_name', 'x_snc_cctcg_photo_profile');
qc.addOrCondition('table_name', 'ZZ_YYx_snc_cctcg_photo_profile');
attachmentGr.addQuery('file_name', 'avatar_without_bg');
}
else if (type == 'qr_code') {
let qc = attachmentGr.addQuery('table_name', 'x_snc_cctcg_photo_qr_code');
qc.addOrCondition('table_name', 'ZZ_YYx_snc_cctcg_photo_qr_code');
attachmentGr.addQuery('file_name', 'qr_code');
}
// ... additional type handling
attachmentGr.addQuery('table_sys_id', parentId);
attachmentGr.query();
if (attachmentGr.next()) {
const attachment = new GlideSysAttachment();
const attachmentContentStream = attachment.getContentStream(attachmentGr.sys_id.toString());
response.setHeader('Content-Disposition', `filename="${parentId}_${type}"`);
response.setContentType('image/png');
response.setStatus(200);
const writer = response.getStreamWriter();
writer.writeStream(attachmentContentStream);
}
})(request, response);
Breaking Down the Binary Stream Process
1. Query for the Attachment
var attachmentGr = new GlideRecord('sys_attachment');
// Custom logic to find the right attachment based on type and parent ID
2. Get the Content Stream
The
getContentStream()
method returns a Java GlideScriptableInputStream that represents the binary file data.
const attachment = new GlideSysAttachment();
const attachmentContentStream = attachment.getContentStream(attachmentGr.sys_id.toString());
3. Set Response Headers
The response headers are HTTP headers that allow a browser or other HTTP client to get additional data about the file being transferred. The
Content-Disposition
header is being used to set a default file name for the downloaded file. That way, if you navigate to the URL for the REST endpoint from a browser, the file will automatically download with the name of the file used in the Content-Dispsition
header.
response.setHeader('Content-Disposition', `filename="${parentId}_${type}"`);
response.setContentType('image/png');
response.setStatus(200);
4. Write the Binary Data to the stream
Here we connect the Attachment's input stream to the REST API Response's output stream. The result is that the REST API responds with the binary data of the Attachment instead of a JSON payload.
const writer = response.getStreamWriter();
writer.writeStream(attachmentContentStream);
Why Custom File Serving is Useful
- Custom logic: You can implement custom logic such as displaying different images depending on the state of a record and house that logic on the server, keeping it consistent.
- Public access options: We could make certain images publicly viewable without authentication when needed and without needing to alter table ACLs.
- Custom headers: Control cache behavior, file names, and content types exactly how you want
- Usage tracking: Log or monitor access to specific images / files if needed
Technique 3: Creating String Based Files (like CSV)
The third challenge was creating a CSV export for our printing vendor. This shows how ServiceNow REST APIs can handle any text-based content type (even files like PowerPoint, Excel, Word, etc), not just JSON. And to be clear, these are files that can be downloaded, not simply text in place of JSON in the response.
(function process(request, response) {
var encodedQuery = request.queryParams.query || 'avatar_without_bg!=';
var profileGR = new GlideRecord('x_snc_cctcg_photo_profile');
profileGR.addEncodedQuery(encodedQuery);
profileGR.setLimit(10);
profileGR.query();
var csvContent = '"Name","Email","Alternate Email","Special Card","Badge 1","Badge 2","Badge 3","First ServiceNow Release","Flavored persona","Is MVP","Rank ID","Technical Superpower","Team","Avatar","Avatar without Bg","Physical Card","Digital Card","Physical QR Code","Digital QR Code"\n';
while (profileGR.next()) {
var qrGR = new GlideRecord('x_snc_cctcg_photo_qr_code');
var physicalQR = '';
var digitalQR = '';
// Get associated QR codes
qrGR.addQuery('profile', profileGR.getUniqueValue());
qrGR.query();
while (qrGR.next()) {
if (qrGR.qr_type.toString() == 'de165405476cee50e52659db416d436d') {
physicalQR = qrGR.sys_id.toString();
} else if (qrGR.qr_type.toString() == 'c4265405476cee50e52659db416d4371') {
digitalQR = qrGR.sys_id.toString();
}
}
// Build CSV row with image URLs
var row = [
'"' + profileGR.name.toString() + '"',
'"' + profileGR.email.toString() + '"',
// ... other fields
(!profileGR.avatar_without_bg.nil() ? '"' + gs.getProperty('glide.servlet.uri') + '/api/x_snc_cctcg_photo/creatorcon_c3_webhooks/image?parentId=' + profileGR.getUniqueValue() + '&type=avatar_without_bg"' : '""'),
(!profileGR.card_with_qr.nil() ? '"' + gs.getProperty('glide.servlet.uri') + '/api/x_snc_cctcg_photo/creatorcon_c3_webhooks/image?parentId=' + profileGR.getUniqueValue() + '&type=card_physical"' : '""'),
// ... more image URL fields
].join(',');
csvContent += row + '\n';
}
// Output CSV content
response.setHeader('Content-Type', 'text/csv');
response.setHeader('Content-Disposition', 'attachment; filename=card_export.csv');
response.setStatus(200);
response.getStreamWriter().writeString(csvContent);
})(request, response);
Breaking down the CSV Export
1. Custom Content-Type Headers
This tells browsers to download the response as a CSV file rather than trying to display it.
response.setHeader('Content-Type', 'text/csv');
response.setHeader('Content-Disposition', 'attachment; filename=card_export.csv');
2. Build a CSV row
Each row of the CSV is built as an array of strings and then concatenated with a comma (because comma separated values and all that).
// Build CSV row with image URLs
var row = [
'"' + profileGR.name.toString() + '"',
'"' + profileGR.email.toString() + '"',
// ... other fields
(!profileGR.avatar_without_bg.nil() ? '"' + gs.getProperty('glide.servlet.uri') + '/api/x_snc_cctcg_photo/creatorcon_c3_webhooks/image?parentId=' + profileGR.getUniqueValue() + '&type=avatar_without_bg"' : '""'),
(!profileGR.card_with_qr.nil() ? '"' + gs.getProperty('glide.servlet.uri') + '/api/x_snc_cctcg_photo/creatorcon_c3_webhooks/image?parentId=' + profileGR.getUniqueValue() + '&type=card_physical"' : '""'),
// ... more image URL fields
].join(',');
3. Build up a string value
We build up a string value that will serve as the content of the file. So, we set it up initially with CSV headers and then incrementally add each row to the string.
var csvContent = '"Name","Email","Alternate Email","Special Card","Badge 1","Badge 2","Badge 3","First ServiceNow Release","Flavored persona","Is MVP","Rank ID","Technical Superpower","Team","Avatar","Avatar without Bg","Physical Card","Digital Card","Physical QR Code","Digital QR Code"\n';
// Other logic
// For each row
csvContent += row + '\n';
4. Including Image URLs
The CSV includes full URLs back to our image serving REST API, so external systems can download the actual image files... because why wouldn't we connect one fancy file handling REST API to another?
'"' + gs.getProperty('glide.servlet.uri') + '/api/x_snc_cctcg_photo/creatorcon_c3_webhooks/image?parentId=' + profileGR.getUniqueValue() + '&type=avatar_without_bg"'
5. String Output Instead of JSON
Rather than using
response.setBody()
with a JSON object, we're directly writing the CSV string to the response stream.
response.getStreamWriter().writeString(csvContent);
Conclusion
Working with binary files and alternative content types in ServiceNow showed us that the platform's REST capabilities are way more flexible than many of us may realize. We're not limited to JSON APIs... Scripted REST APIs can handle pretty much any content type you need.
This opens possibilities for:
- File processing workflows that fetch, transform, and serve binary content (might need some MID server help with this one)
- Custom export formats tailored to specific external system requirements
- Flexible security models that go beyond traditional record-based permissions
- Integration patterns that work with any external system, regardless of their preferred data formats
The key insight is that ServiceNow's Scripted REST APIs are built on standard HTTP foundations. Once you understand how to work with streams, headers, and content types, you can implement almost any integration pattern you need.
Sure, JSON is great for most API work, but when you need to go beyond JSON, ServiceNow has the tools to handle whatever content types your integrations require. And honestly, I thought that was pretty dang cool
- 72 Views
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.