Skip to Content
  1. Home
  2. /
  3. Blog
  4. /
  5. ServiceNow Scripted REST APIs: Complete Developer Guide
Monday, June 22, 2026

ServiceNow Scripted REST APIs: Complete Developer Guide

ServiceNow Scripted REST APIs: Complete Developer Guide

ServiceNow's Scripted REST API framework lets you build custom REST endpoints inside the Now Platform. Unlike REST Messages (which are for outbound calls), Scripted REST APIs turn your ServiceNow instance into an API server — receiving and responding to requests from any HTTP client.

Most developers learn the basics and stop there. That leaves production systems fragile: no proper error codes, no authentication, no pagination, and performance that craters under load. This guide covers the patterns that separate hobby projects from hardened, production-grade APIs.

Creating Your First Scripted REST API

Navigate to System Web Services > Scripted REST APIs and create a new record:

Name: Incident Management API
Namespace: x_mycompany_incident
Base path: /incident

Then add a resource. The resource defines a method + path combination:

Name: List Open Incidents
HTTP Method: GET
Path: /open

Here's the script that handles it:

JavaScript
(function process(/*RESTRequest*/ request, /*RESTResponse*/ response) {
    var gr = new GlideRecord('incident');
    gr.addQuery('active', true);
    gr.addQuery('state', '!=', 7); // not resolved/closed
    gr.orderByDesc('opened_at');
    gr.setLimit(100);
    gr.query();

    var results = [];
    while (gr.next()) {
        results.push({
            sys_id: gr.sys_id.toString(),
            number: gr.number.toString(),
            short_description: gr.short_description.toString(),
            state: gr.state.getDisplayValue(),
            assigned_to: gr.assigned_to.getDisplayValue(),
            opened_at: gr.opened_at.getValue()
        });
    }

    response.setStatus(200);
    return results;
})(request, response);

That works. But production APIs need more.

Authentication Patterns

Never leave an API unauthenticated unless it's intentionally public. Here are the three patterns you'll use most.

Basic Authentication

JavaScript
(function process(request, response) {
    var auth = request.getHeader('Authorization');
    if (!auth || !auth.startsWith('Basic ')) {
        response.setStatus(401);
        response.setHeader('WWW-Authenticate', 'Basic realm="ServiceNow"');
        return { error: 'Authentication required' };
    }

    var decoded = new GlideEncdec().base64Decode(auth.slice(6));
    var parts = decoded.split(':', 2);
    var username = parts[0];
    var password = parts[1];

    // Validate against ServiceNow users
    var gr = new GlideRecord('sys_user');
    gr.addQuery('user_name', username);
    gr.addQuery('active', true);
    gr.query();
    if (!gr.next()) {
        response.setStatus(401);
        return { error: 'Invalid credentials' };
    }

    // Continue with request processing...
})(request, response);

Note: Always use HTTPS. Basic auth over plain HTTP sends credentials in clear text.

API Key Authentication

For machine-to-machine integrations where username/password is impractical:

JavaScript
(function process(request, response) {
    var apiKey = request.getHeader('x-api-key');
    if (!apiKey) {
        response.setStatus(401);
        return { error: 'API key required. Pass x-api-key header.' };
    }

    var gr = new GlideRecord('api_key');
    gr.addQuery('key', apiKey);
    gr.addQuery('active', true);
    gr.query();

    if (!gr.next()) {
        response.setStatus(401);
        return { error: 'Invalid API key' };
    }

    // Store the key record for potential rate limiting or logging
    request.apiKeyRecord = gr;
})(request, response);

Store your API keys in a dedicated api_key table with fields for the key value, associated user/service, active flag, and optional rate limits.

OAuth 2.0 Bearer Token

For more sophisticated auth flows, validate OAuth access tokens:

JavaScript
(function process(request, response) {
    var authHeader = request.getHeader('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        response.setStatus(401);
        return { error: 'Bearer token required' };
    }

    var token = authHeader.slice(7);
    var tokenRecord = new GlideRecord('oauth_token');
    tokenRecord.addQuery('token', token);
    tokenRecord.addQuery('active', true);
    tokenRecord.addQuery('sys_created_on', '>', gs.daysAgo(1));
    tokenRecord.query();

    if (!tokenRecord.next()) {
        response.setStatus(401);
        return { error: 'Invalid or expired token' };
    }
})(request, response);

Handling POST and PUT with Request Body Parsing

For methods that receive JSON payloads:

JavaScript
function handlePost(request, response) {
    var payload = request.body;
    if (!payload) {
        response.setStatus(400);
        return { error: 'Request body required' };
    }

    var data;
    try {
        data = JSON.parse(payload);
    } catch (e) {
        response.setStatus(400);
        return { error: 'Invalid JSON: ' + e.message };
    }

    // Validate required fields
    var required = ['short_description', 'caller_id'];
    for (var i = 0; i < required.length; i++) {
        if (!data[required[i]]) {
            response.setStatus(400);
            return { error: 'Missing required field: ' + required[i] };
        }
    }

    var gr = new GlideRecord('incident');
    gr.initialize();
    gr.short_description = data.short_description;
    gr.caller_id = data.caller_id;
    gr.urgency = data.urgency || 3;
    gr.impact = data.impact || 2;
    gr.insert();

    response.setStatus(201);
    return {
        sys_id: gr.sys_id.toString(),
        number: gr.number.toString(),
        message: 'Incident created successfully'
    };
}

Always validate before inserting. Never trust the incoming payload — malicious or malformed data is a real-world threat.

Pagination: The Right Way

Returning unbounded result sets is a performance killer. Here's cursor-based pagination using sys_id:

JavaScript
function listIncidents(request, response) {
    var limit = parseInt(request.queryParams.limit) || 20;
    var cursor = request.queryParams.cursor; // last seen sys_id

    var gr = new GlideRecord('incident');
    gr.addQuery('active', true);

    if (cursor) {
        gr.addQuery('sys_id', '>', cursor);
    }

    gr.orderBy('sys_id');
    gr.setLimit(limit + 1); // fetch one extra to check if there's a next page
    gr.query();

    var results = [];
    var nextCursor = null;
    var count = 0;

    while (gr.next()) {
        count++;
        if (count <= limit) {
            results.push({
                sys_id: gr.sys_id.toString(),
                number: gr.number.toString(),
                short_description: gr.short_description.toString()
            });
            nextCursor = gr.sys_id.toString();
        }
    }

    response.setStatus(200);
    response.setHeader('X-Has-Next-Page', (count > limit).toString());

    return {
        data: results,
        pagination: {
            next_cursor: (count > limit) ? nextCursor : null,
            limit: limit
        }
    };
}

Clients consume this by storing the next_cursor and passing it back as the cursor query param on the next request. This is far more efficient than offset-based pagination on large tables.

Error Handling and HTTP Status Codes

Return the right status code so clients can handle errors programmatically:

ScenarioStatus Code
Success200 / 201 / 204
Validation error400
Authentication failed401
Forbidden / insufficient permissions403
Resource not found404
Conflict (duplicate)409
Rate limited429
Server error500
JavaScript
function safeQuery(request, response) {
    try {
        var gr = new GlideRecord('incident');
        gr.addQuery('sys_id', request.pathParams.sys_id);
        gr.query();

        if (!gr.next()) {
            response.setStatus(404);
            return { error: 'Incident not found', sys_id: request.pathParams.sys_id };
        }

        response.setStatus(200);
        return { /* incident data */ };

    } catch (e) {
        gs.error('Incident API error: ' + e.message);
        response.setStatus(500);
        return { error: 'Internal server error', reference: gs.generateGUID() };
    }
}

Always generate a reference ID for 500 errors. When clients report an issue, that ID lets you grep your logs instantly.

Performance Tips

A few things that make a massive difference under load:

1. Only query fields you need.

JavaScript
gr.selectFields(['sys_id', 'number', 'short_description', 'state']);

2. Cache reference field display values — don't call .getDisplayValue() in a loop.

JavaScript
var gr = new GlideRecord('incident');
// ... query ...
var grRef = new GlideRecord('sys_user');
while (gr.next()) {
    // One query per row = N+1 problem
    // gr.assigned_to.getDisplayValue(); // avoid inside loop
}
// Instead, batch-load reference records before the loop

3. Use GlideAggregate for counts:

JavaScript
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
ga.next();
var total = ga.getAggregate('COUNT');

4. Set query timeouts on expensive endpoints:

JavaScript
gr.setLimit(100);
gr.query();
gs.sleep(0); // yield to prevent instance watchdog kills on long queries

Versioning Your API

As your API evolves, maintain backward compatibility by versioning:

Base path: /incident/v1
Base path: /incident/v2

Keep the old version running until all consumers migrate. Document the deprecation timeline in your API response headers:

JavaScript
response.setHeader('Deprecation', 'true');
response.setHeader('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT');
response.setHeader('Link', '</incident/v2/open>; rel="successor-version"');

Testing Your Scripted REST API

Use an HTTP client to test. In ServiceNow, the REST API Explorer (under System Web Services > REST API Explorer) generates test calls with proper auth. For local testing, curl works:

Bash
# GET with Basic auth
curl -X GET "https://yourinstance.service-now.com/api/x_mycompany_incident/incident/open" \
  -u admin:password \
  -H "Accept: application/json"

# POST with API key
curl -X POST "https://yourinstance.service-now.com/api/x_mycompany_incident/incident" \
  -H "x-api-key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"short_description": "Test incident", "caller_id": "681ccaf9c0a8016400b98b0686d73a2f"}'

Closing Thoughts

Scripted REST APIs are one of ServiceNow's most powerful integration features. The basics are easy to learn, but the details — authentication, error handling, pagination, performance — are what separate a proof-of-concept from a system that other teams trust with their critical integrations.

Start with the patterns above: auth first, proper error codes, pagination on everything that returns lists, and query limits on everything. Build those in from day one rather than bolting them on later.

Your future self — and the teams integrating with your API — will thank you.

Was this article helpful?