Building Scripted REST APIs in ServiceNow: A Complete Guide

How to design, build, and secure custom REST endpoints in ServiceNow — resource design, request/response handling, versioning, and authentication patterns.

The Table API covers most data access scenarios, but sometimes you need a custom endpoint — one that runs business logic, aggregates data from multiple tables, or exposes an operation that doesn't map cleanly to CRUD. Scripted REST APIs are how you build those endpoints on the ServiceNow platform.

When to Build a Scripted REST API

Build a Scripted REST API when:

  • You need to execute business logic as part of the API call (not just data retrieval)
  • You need to aggregate data from multiple tables into a single response
  • You're exposing ServiceNow functionality to an external system that expects a specific API contract
  • You need custom authentication or rate limiting beyond what the Table API provides

Creating a Scripted REST API

Navigate to System Web Services > Scripted REST APIs > New

Key settings:

  • API ID — used in the URL path: /api/<scope>/<api_id>
  • API Namespace — your application scope prefix
  • Requires Authentication — almost always true for production APIs

Resource Design

A Scripted REST API is made up of Resources — individual endpoints with their own path and HTTP method handling.

// API: /api/nowspec/incident_metrics
// Resources:
//   GET  /summary          → overall metrics
//   GET  /by_group/{id}    → metrics for specific group
//   POST /refresh          → trigger metrics recalculation

Use path parameters for resource identifiers and query parameters for filters:

// Path parameter: /api/nowspec/incidents/{sys_id}
var sys_id = request.pathParams.sys_id;

// Query parameter: /api/nowspec/incidents?priority=1&limit=50
var priority = request.queryParams.priority;
var limit = parseInt(request.queryParams.limit) || 25;

Request and Response Handling

(function process(request, response) {
    
    // Read request body (for POST/PUT)
    var body = JSON.parse(request.body.dataString);
    
    // Set response status
    response.setStatus(200); // or 201, 400, 404, 500
    
    // Set content type
    response.setContentType('application/json');
    
    // Build response object
    var result = {
        status: 'success',
        data: {},
        meta: {
            timestamp: new Date().toISOString(),
            version: '1.0'
        }
    };
    
    // Execute business logic
    var gr = new GlideRecord('incident');
    gr.addEncodedQuery('active=true^priority=1');
    gr.query();
    
    var incidents = [];
    while (gr.next()) {
        incidents.push({
            sys_id: gr.getUniqueValue(),
            number: gr.getValue('number'),
            short_description: gr.getValue('short_description'),
            assigned_to: gr.getDisplayValue('assigned_to')
        });
    }
    
    result.data = incidents;
    result.meta.count = incidents.length;
    
    response.setBody(JSON.stringify(result));
    
})(request, response);

Error Handling Pattern

(function process(request, response) {
    
    try {
        var sys_id = request.pathParams.sys_id;
        
        if (!sys_id) {
            response.setStatus(400);
            response.setBody(JSON.stringify({
                error: 'sys_id is required',
                code: 'MISSING_PARAMETER'
            }));
            return;
        }
        
        var gr = new GlideRecord('incident');
        if (!gr.get(sys_id)) {
            response.setStatus(404);
            response.setBody(JSON.stringify({
                error: 'Incident not found',
                code: 'RECORD_NOT_FOUND'
            }));
            return;
        }
        
        // Success response
        response.setStatus(200);
        response.setBody(JSON.stringify({
            sys_id: gr.getUniqueValue(),
            number: gr.getValue('number')
        }));
        
    } catch(e) {
        gs.error('Scripted REST API error: ' + e.message);
        response.setStatus(500);
        response.setBody(JSON.stringify({
            error: 'Internal server error',
            code: 'SERVER_ERROR'
        }));
    }
    
})(request, response);

Versioning

ServiceNow Scripted REST APIs support versioning through the API definition. When you need to make a breaking change:

  1. Create a new version of the API (v2)
  2. Build the new version alongside the existing one
  3. Communicate a deprecation timeline to API consumers
  4. Remove v1 only after all consumers have migrated

The URL includes the version: /api/nowspec/incidents/v2/summary

Security Considerations

  • Always require authentication — never expose a Scripted REST API without it
  • Validate all input parameters before using them in queries
  • Never concatenate user input directly into encoded queries — use addQuery() instead
  • Limit what data is returned based on the calling user's roles, not just authentication
  • Log API calls including the caller's user ID for audit purposes

Request and response objects — full reference

The request object in a Scripted REST API script provides all inbound request data. Key properties and methods:

// Path parameters: /api/x_myapp/api/resource/{sys_id}
var sysId = request.pathParams.sys_id;

// Query parameters: ?status=active&limit=50  
var status = request.queryParams.status;
var limit = parseInt(request.queryParams.limit) || 50;

// Request body (JSON)
var body = JSON.parse(request.body.dataString);

// Request headers
var authHeader = request.getHeader('Authorization');
var contentType = request.getHeader('Content-Type');

// HTTP method
var method = request.getRequestMethod(); // "GET", "POST", etc.

// Response object
response.setStatus(200);
response.setBody({ result: 'success', data: yourData });
response.setHeader('X-Custom-Header', 'value');

Building a robust POST endpoint

// POST endpoint with full validation and error handling
(function process(request, response) {
    // Validate content type
    var ct = request.getHeader('Content-Type') || '';
    if (ct.indexOf('application/json') === -1) {
        response.setStatus(415); // Unsupported Media Type
        response.setBody({ error: 'Content-Type must be application/json' });
        return;
    }
    
    // Parse and validate body
    var body;
    try { body = JSON.parse(request.body.dataString); }
    catch(e) {
        response.setStatus(400);
        response.setBody({ error: 'Invalid JSON body' });
        return;
    }
    
    // Validate required fields
    if (!body.short_description || !body.category) {
        response.setStatus(422);
        response.setBody({ 
            error: 'Missing required fields', 
            required: ['short_description', 'category'] 
        });
        return;
    }
    
    // Create record
    var gr = new GlideRecord('incident');
    gr.initialize();
    gr.setValue('short_description', body.short_description);
    gr.setValue('category', body.category);
    gr.setValue('caller_id', gs.getUserID());
    var newSysId = gr.insert();
    
    if (!newSysId) {
        response.setStatus(500);
        response.setBody({ error: 'Failed to create record' });
        return;
    }
    
    response.setStatus(201); // Created
    response.setBody({ 
        result: 'created',
        sys_id: newSysId,
        number: gr.getValue('number')
    });
})(request, response);

Related: Scripted REST API guide · REST API overview · OAuth 2.0 · GlideRecord performance

Request Object: Reading Parameters and Body

The request object in a Scripted REST API resource script provides access to all inbound data. Path parameters defined in the resource URL pattern (e.g., /api/myapp/tickets/{id}) are accessed via request.pathParams.id. Query string parameters are accessed via request.queryParams.parameter_name — note that query params return arrays by default since the same parameter can appear multiple times. The request body for POST and PUT requests is accessed via request.body.data for JSON bodies (automatically parsed) or request.body.dataString for raw string access. Always validate that required parameters are present and return a 400 response with a clear error message if they are missing — silent failures where a missing parameter causes unexpected behaviour are difficult for API consumers to debug.

Response Object: Status Codes and Headers

The response object controls the HTTP response. Set response.setStatus(200) explicitly for successful responses — the default may not match what the calling system expects. For REST APIs following conventional patterns, return 200 for successful GETs, 201 for successful creates, 204 for successful deletes with no body, 400 for invalid input, 404 for not-found resources, and 500 for unexpected server errors. Add custom headers with response.headers.header_name = value. For paginated endpoints, include pagination metadata in response headers (X-Total-Count, X-Page) following the pattern used by the standard Table API. The HTTP status codes guide covers the full semantics of each code class.

Authentication and Scope

Scripted REST APIs inherit ServiceNow's standard authentication — callers authenticate via Basic Auth, OAuth token, or session cookie the same way they would call the Table API. The resource script runs in the scope of the calling user's session, which means ACLs and role checks apply normally. If your API needs to perform operations that the calling user's role does not permit, you must use gs.elevatePrivilege() carefully or restructure the API to avoid requiring elevated access for standard operations. For APIs designed to be called by integration users with minimal roles, explicitly test with a restricted user account to verify the ACL behaviour matches expectations before deploying.

Versioning and Backward Compatibility

Scripted REST APIs support versioning through the API version field on the resource, resulting in URLs like /api/myapp/v1/tickets and /api/myapp/v2/tickets. Version two of an API can coexist with version one, enabling non-breaking API evolution: new consumers use v2, existing consumers continue on v1 until they are migrated. When deciding whether a change requires a new version, the rule is simple — if removing or renaming a response field, or changing a parameter's accepted values, create a new version. If adding optional fields or parameters, you can extend the existing version. Document version deprecation timelines explicitly and monitor API call volumes by version using the GlideAggregate COUNT pattern against the transaction log to know when it is safe to retire old versions.

Logging and Observability

Add structured logging to every Scripted REST API resource to support production debugging. Log the inbound request parameters, the outcome (success or specific failure reason), and execution time at minimum. Use gs.info() for normal operation logs and gs.error() for failures — these go to the system log with appropriate severity levels. For APIs that process data, log the count of records affected rather than individual record details (to avoid filling logs with verbose output in high-volume scenarios). Good logging transforms a mysterious integration failure report from an external team into a diagnosed, understood event within minutes. The debugging guide covers how to filter and read these logs efficiently during incident investigations.

Pagination Implementation

Scripted REST APIs that return potentially large result sets must implement pagination. The standard pattern mirrors the Table API: accept limit and offset query parameters, apply them to the GlideRecord query using gr.setLimit(limit) and gr.chooseWindow(offset, offset + limit), and return pagination metadata in response headers. Include a Link header with rel="next" pointing to the next page URL — this enables calling systems to auto-paginate without calculating offsets themselves. Document the maximum supported limit value and return a 400 error if it is exceeded, preventing callers from requesting unbounded result sets that could time out or cause memory pressure. The performance guide covers the GlideRecord window query pattern.

Cross-Origin Resource Sharing (CORS) Configuration

If your Scripted REST API will be called from browser-based JavaScript (a custom portal, an external web app), CORS configuration is required. ServiceNow manages CORS rules through the REST API CORS Rules module — you define which external origins are permitted to call which API namespaces. Without a matching CORS rule, browser-originated API calls will fail with a CORS error regardless of whether the authentication and authorisation are correct. Server-to-server API calls (from another backend system) are not subject to CORS and do not need this configuration. For APIs that serve both browser and server consumers, add CORS rules for the browser origins while keeping the API authentication requirements the same for both callers. Document which of your APIs have CORS enabled, since unrestricted CORS rules (* origin) expose the API to any web page and should be avoided for APIs that return sensitive data.

Want the complete reference?

This article is part of the NowSpectrum knowledge library. Browse all products for cheat sheets, interview prep, and deep-dive reference guides.

Browse All Products →
← Back to all posts