The Zurich release has arrived! Interested in new features and functionalities? Click here for more

Travis Toulson
Administrator
Administrator

Beyond JSON Working with Binary Files and Alternative Content Types in ServiceNow REST APIs.png

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:

 

  1. Makes the GET request to fetch the binary image data
  2. Automatically handles the binary stream without you having to deal with base64 encoding
  3. Creates the attachment record in the sys_attachment table
  4. 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