What It Is

Scripted REST APIs are custom REST endpoints that expose ServiceNow platform functionality to external systems through HTTP requests. Unlike the standard Table API or Attachment API, Scripted REST APIs give you complete control over the request processing logic, data validation, response formatting, and security model. They're stored in the sys_ws_definition table and consist of one or more resources that handle specific HTTP methods like GET, POST, PUT, or DELETE. Each resource contains JavaScript code that processes the incoming request and generates the response, making them the most flexible integration option for complex business logic or non-standard data operations.

Architecturally, Scripted REST APIs live within the Web Services framework in ServiceNow, accessible through the System Web Services > Scripted REST APIs module. They execute in the same JavaScript engine as Business Rules and Script Includes, with access to all server-side APIs including GlideRecord, GlideSystem, and custom Script Includes. The execution happens at the application layer, after authentication but before any table-level access controls, giving you the ability to implement custom security logic. When a request hits your endpoint at /api/{namespace}/{api_name}/{resource_path}, ServiceNow routes it to your script where you control everything from authentication to response codes.

The underlying data model treats each Scripted REST API as a definition record with associated resource records in sys_ws_operation. The definition establishes the namespace, base path, and security settings, while each resource defines the specific endpoint behavior for different HTTP methods. ServiceNow's web service framework handles the HTTP parsing, authentication, and basic routing, then passes control to your JavaScript execution context. This means your scripts run with the privileges of the authenticated user unless you explicitly elevate them, and they have access to the full request and response objects for complete HTTP control.

You cannot function without Scripted REST APIs when external systems need to perform complex operations that don't map cleanly to CRUD operations on individual tables. This includes scenarios like bulk data operations with custom validation logic, multi-table transactions that must succeed or fail atomically, or integration patterns that require custom authentication beyond basic auth or OAuth. When a partner system needs to submit a complete service request with attachments, approvals, and related tasks in a single API call, the Table API falls short because it can't coordinate the complex business logic across multiple tables. Similarly, when you need to expose calculated data that combines information from multiple sources or requires real-time processing, Scripted REST APIs provide the only viable path to maintain data consistency and business rule enforcement.

Platform owners and integration architects typically design the API structure and security model, while developers implement the actual JavaScript logic within each resource. Administrators manage the operational aspects like monitoring API usage through Stats records, configuring rate limiting, and maintaining user access to the underlying data. The relationship between these roles becomes critical because a poorly designed API can expose sensitive data or create performance bottlenecks that affect the entire platform. Developers need to understand both the business requirements and the technical constraints of the ServiceNow environment, while administrators must monitor API performance and ensure security policies are properly enforced at both the API level and the data level.

Recent ServiceNow releases have enhanced Scripted REST APIs with improved debugging capabilities in Studio, better integration with OAuth 2.0 endpoints, and enhanced support for OpenAPI documentation generation. Vancouver introduced more robust error handling patterns and improved the RESTAPIRequest and RESTAPIResponse objects with additional methods for handling complex content types. Xanadu expanded the debugging experience with better stack trace information and introduced performance monitoring hooks that integrate with Performance Analytics. These improvements make it easier to troubleshoot API issues in production and provide better visibility into how external systems are using your endpoints.

Where to Find and Configure It

The primary configuration interface is at System Web Services > Scripted REST APIs where you create and manage API definitions. In Studio, access them through Create Application File > Web Service > Scripted REST API to build APIs within a specific application scope. App Engine Studio provides a simplified interface under Add > Integration > REST API for citizen developers building basic endpoints.

Direct table access is available at sys_ws_definition.list for API definitions and sys_ws_operation.list for individual resource methods, useful for bulk operations or advanced filtering. Monitor API usage through System Logs > REST API and performance data in System Diagnostics > Stats filtered by API name. Scoped applications contain their APIs within the application scope, making them portable across instances, while global APIs are available system-wide but cannot be easily moved between environments.

How It Works Step by Step

When an external system makes an HTTP request to your Scripted REST API endpoint, ServiceNow's web service framework first validates the request against the API definition's security settings and routing rules. The system authenticates the user, checks their access to the API namespace, and parses the incoming HTTP request into JavaScript objects that your code can manipulate. If authentication fails or the user lacks the required roles, the request terminates before reaching your script.

Once validation passes, ServiceNow creates the execution context with the authenticated user's privileges and initializes the request and response objects containing all HTTP data and methods for response generation. Your JavaScript code executes with full access to the platform APIs, processing the request data, performing business logic, and constructing the response. The execution follows standard JavaScript scoping rules, with access to global Script Includes and the ability to call other platform functions, but runs within the security context of the authenticated user unless explicitly elevated.

After your script completes, ServiceNow serializes the response data and returns it to the calling system with the appropriate HTTP status code and headers. Any uncaught exceptions in your script result in a 500 error response, while explicit response codes set through the response object are honored. The platform logs the transaction details for monitoring and debugging, including execution time, response codes, and any errors that occurred during processing.

Free Newsletter

Enjoying this? Get one deep-dive per week.

Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.

No spam · Unsubscribe anytime

The Execution Order

  1. HTTP request arrives at ServiceNow and gets routed to the web service framework
  2. System validates the API namespace and path against registered Scripted REST API definitions
  3. Authentication occurs using the method defined in the API (basic auth, OAuth, etc.)
  4. System checks user roles against the API's required roles configuration
  5. HTTP method gets matched to the appropriate resource operation (GET, POST, etc.)
  6. ServiceNow creates the JavaScript execution context with request/response objects
  7. Your resource script executes with access to all platform APIs and user privileges
  8. Response object gets serialized and returned with appropriate HTTP status and headers
incident_api_get.js
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    
    var incidentSysId = request.pathParams.sys_id;
    
    if (!incidentSysId) {
        response.setStatus(400);
        response.setBody({error: "sys_id parameter required"});
        return;
    }
    
    var inc = new GlideRecord('incident');
    if (!inc.get(incidentSysId)) {
        response.setStatus(404);
        response.setBody({error: "Incident not found"});
        return;
    }
    
    response.setBody({
        number: inc.getDisplayValue('number'),
        state: inc.getDisplayValue('state'),
        short_description: inc.getDisplayValue('short_description'),
        assigned_to: inc.getDisplayValue('assigned_to')
    });
    
})(request, response);

Real-World Scenarios

Creating a Bulk User Import API with Validation

Your HR system needs to push batches of new employee data to ServiceNow, but each user record requires validation against Active Directory and custom business rules that don't work with the standard Table API. The import must be atomic—either all users succeed or none do—and needs to return detailed validation errors for each failed record.

bulk_user_import.js
(function process(request, response) {
    var users = request.body.data.users;
    var results = [];
    var errors = [];
    
    // Validate all users first
    for (var i = 0; i < users.length; i++) {
        var user = users[i];
        var validation = validateUser(user);
        if (!validation.valid) {
            errors.push({index: i, email: user.email, errors: validation.errors});
        }
    }
    
    if (errors.length > 0) {
        response.setStatus(400);
        response.setBody({success: false, validation_errors: errors});
        return;
    }
    
    // If validation passes, create all users
    for (var j = 0; j < users.length; j++) {
        var userRec = new GlideRecord('sys_user');
        userRec.initialize();
        userRec.email = users[j].email;
        userRec.first_name = users[j].first_name;
        userRec.last_name = users[j].last_name;
        userRec.user_name = users[j].email;
        var sysId = userRec.insert();
        results.push({email: users[j].email, sys_id: sysId});
    }
    
    response.setBody({success: true, created_users: results});
})(request, response);

Create this as a POST resource in your API definition and ensure the user_admin role is required for access. You'll need to implement the validateUser function as a Script Include to check email format, duplicate detection, and department validation. Watch for transaction limits—if you're processing more than 100 users at once, consider implementing pagination or background processing to avoid timeout issues.

Building a Service Request with Attachments API

External portal users need to submit service requests that include file attachments, but your portal framework can't handle the complex workflow of creating the request, attaching files, and triggering approval workflows in the correct sequence. The API must accept base64-encoded files and ensure all components are properly linked before triggering downstream processes.

service_request_with_attachments.js
(function process(request, response) {
    var reqData = request.body.data;
    
    // Create the service request
    var sr = new GlideRecord('sc_request');
    sr.initialize();
    sr.requested_for = reqData.requested_for;
    sr.short_description = reqData.short_description;
    sr.description = reqData.description;
    sr.urgency = reqData.urgency || 3;
    var requestSysId = sr.insert();
    
    if (!requestSysId) {
        response.setStatus(500);
        response.setBody({error: "Failed to create service request"});
        return;
    }
    
    // Process attachments
    var attachmentIds = [];
    if (reqData.attachments && reqData.attachments.length > 0) {
        for (var i = 0; i < reqData.attachments.length; i++) {
            var att = reqData.attachments[i];
            var attachment = new GlideSysAttachment();
            var attachId = attachment.writeBase64(sr, att.filename, att.content_type, att.base64_data);
            if (attachId) {
                attachmentIds.push(attachId);
            }
        }
    }
    
    response.setBody({
        request_number: sr.getDisplayValue('number'),
        sys_id: requestSysId,
        attachments_created: attachmentIds.length
    });
})(request, response);

Set this up as a POST resource with appropriate content-type handling for JSON payloads. Ensure your API definition allows larger request sizes in the Max request size field to accommodate base64-encoded files. Be careful with attachment size limits and consider implementing file type validation to prevent security issues—base64 encoding increases file size by about 33%, so factor that into your size calculations.

Custom Authentication with Rate Limiting

Your API needs to authenticate requests using custom tokens that aren't standard OAuth, and you need to implement rate limiting per client to prevent abuse. Each client has a unique API key stored in a custom table, and you want to allow only 100 requests per hour per client.

custom_auth_with_rate_limit.js
(function process(request, response) {
    var apiKey = request.getHeader('X-API-Key');
    
    if (!apiKey) {
        response.setStatus(401);
        response.setBody({error: "API key required"});
        return;
    }
    
    // Validate API key
    var client = new GlideRecord('x_custom_api_clients');
    client.addQuery('api_key', apiKey);
    client.addQuery('active', true);
    client.query();
    
    if (!client.next()) {
        response.setStatus(401);
        response.setBody({error: "Invalid API key"});
        return;
    }
    
    // Check rate limiting
    var rateLimitCheck = new RateLimitUtil();
    if (!rateLimitCheck.checkLimit(client.getUniqueValue(), 100, 3600)) {
        response.setStatus(429);
        response.setBody({error: "Rate limit exceeded"});
        return;
    }
    
    // Process the actual request
    var data = processBusinessLogic(request.body.data);
    response.setBody(data);
    
})(request, response);

Implement this by setting the API's authentication to No authentication required and handling authentication entirely in your script. You'll need to create a RateLimitUtil Script Include that tracks request counts per client using a custom table or leveraging ServiceNow's built-in statistics framework. Watch for the security implications of bypassing standard authentication—ensure your custom validation is bulletproof and consider logging all authentication attempts for security monitoring.

The Classic Mistake

⚠️

Using request.body.dataString without proper JSON validation, causing scripts to fail silently on malformed payloads.

BAD: Unsafe JSON parsing
(function process(request, response) {
    // This will break on malformed JSON
    var payload = JSON.parse(request.body.dataString);
    
    var gr = new GlideRecord('incident');
    gr.initialize();
    gr.short_description = payload.description;
    gr.priority = payload.priority;
    gr.assignment_group = payload.group;
    var sysId = gr.insert();
    
    response.setStatus(201);
    response.setHeader('Content-Type', 'application/json');
    response.getStreamWriter().writeString(JSON.stringify({
        result: 'created',
        sys_id: sysId
    }));
})(request, response);

When malformed JSON hits this endpoint, the script throws an unhandled exception that returns a generic 500 error to the client with no useful error message. ServiceNow logs the full stack trace in System Log > All, but the client receives only "Internal Server Error" making integration troubleshooting nearly impossible. The script execution stops at the JSON.parse line, so no cleanup or proper error response occurs, leaving both sides in an undefined state.

GOOD: Proper error handling
(function process(request, response) {
    try {
        var payload = JSON.parse(request.body.dataString || '{}');
    } catch (e) {
        response.setStatus(400);
        response.setHeader('Content-Type', 'application/json');
        response.getStreamWriter().writeString(JSON.stringify({
            error: 'Invalid JSON payload',
            message: e.message
        }));
        return;
    }
    
    // Validate required fields
    if (!payload.description || !payload.priority) {
        response.setStatus(400);
        response.setHeader('Content-Type', 'application/json');
        response.getStreamWriter().writeString(JSON.stringify({
            error: 'Missing required fields',
            required: ['description', 'priority']
        }));
        return;
    }
    
    var gr = new GlideRecord('incident');
    gr.initialize();
    gr.short_description = payload.description;
    gr.priority = payload.priority;
    gr.assignment_group = payload.group;
    var sysId = gr.insert();
    
    response.setStatus(201);
    response.setHeader('Content-Type', 'application/json');
    response.getStreamWriter().writeString(JSON.stringify({
        result: 'created',
        sys_id: sysId
    }));
})(request, response);
💡

Always wrap JSON.parse in try-catch and validate all incoming data before processing. Return meaningful HTTP status codes and error messages that help integration developers debug issues.

When to Use This vs Alternatives

Scripted REST APIs are the right choice when you need custom business logic, data transformation, or complex validation that can't be handled by ServiceNow's standard REST API endpoints. Use them when external systems need to integrate with ServiceNow using non-standard data formats or when you need to orchestrate multiple ServiceNow operations in a single API call.

When Scripted REST API is Correct

Choose Scripted REST APIs when you need to transform incoming data before creating records, validate against business rules that extend beyond field validation, or create related records across multiple tables in a single transaction. The standard Table API can't handle complex payloads that map to multiple ServiceNow tables or require custom business logic execution. Import Sets fall short when you need real-time synchronous responses or when the data transformation logic is too complex for transform maps.

When to Use Standard Table API Instead

Use the standard Table API when external systems can send data that maps directly to ServiceNow table structures without transformation. The Table API handles authentication, ACLs, and standard CRUD operations automatically, reducing maintenance overhead. Switch to Import Sets when you need asynchronous processing, built-in error handling and retry logic, or when dealing with large data volumes that would timeout in synchronous REST calls.

When You Need Both Working Together

Combine Scripted REST APIs with Import Sets when you need immediate synchronous validation and response but want asynchronous processing for the actual data creation. Use Scripted REST APIs alongside standard Table API endpoints to provide both simplified custom endpoints for specific integrations and full CRUD capabilities for general use. This approach works well when different consumer systems have varying technical capabilities and integration requirements.

Platform Interactions & Side Effects

  • Business Rules execute normally on any GlideRecord operations within the script, including before/after insert, update, and delete rules that can modify your intended response data
  • ACLs are enforced based on the authenticated user's role, potentially blocking record access even when the script logic appears correct
  • Audit records are written to sys_audit for any table modifications, creating a complete trail of API-driven changes
  • Notifications trigger normally from Business Rules, potentially sending unexpected emails to users when API operations create or modify records
  • Update Sets capture the REST API definition in sys_ws_definition and resources in sys_ws_operation, but script changes require manual migration verification
  • Script execution logs appear in syslog with source com.glide.processors.RESTProcessor for debugging purposes
  • Performance impacts include no automatic caching of responses, and each request spawns a new Rhino script engine context with full platform initialization overhead
  • Session state persists throughout the script execution, meaning user preferences and timezone settings affect date/time processing and GlideRecord queries
  • Transaction rollback using gs.getRollbackOnly() affects all database operations within the script, not just the current GlideRecord operation
  • Client-side Business Rules and UI Policies never execute since the script runs server-side without a form context, potentially missing validation logic

Debugging and Troubleshooting

Common failure symptoms include external systems receiving generic 500 errors with no detail, successful API calls that create no data, or responses that return different data than expected. Admins typically see script errors in System Log > All while integration developers report timeouts or authentication failures. The disconnect between server-side logs and client-side error messages makes root cause analysis challenging without proper logging strategy.

Start troubleshooting in System Log > All filtering by source com.glide.processors.RESTProcessor to see script execution errors and stack traces. Use the Script Debugger by adding gs.log() statements throughout your script to trace execution flow and variable values. Check REST API Explorer under System Web Services > REST to test your endpoints with known payloads.

Look for specific error patterns like "ReferenceError: [variable] is not defined" indicating scope issues with request/response objects, "TypeError: Cannot read property 'dataString' of null" showing missing request body handling, or "IllegalStateException: Response already committed" when trying to set headers after writing response data. Authentication failures appear as "User Not Authenticated" in the system log, while ACL violations show "Security constraints restrict this operation" even when the script logic is correct.

Diagnostic Checklist:

  • Verify the API endpoint is active in sys_ws_definition and all required resources exist in sys_ws_operation
  • Test authentication by checking if gs.getUser().getName() returns the expected user in your script logs
  • Add comprehensive error logging using try-catch blocks around JSON parsing and all GlideRecord operations
  • Check ACL permissions by impersonating the API user and manually testing record access through the ServiceNow interface
  • Validate request format by logging request.body.dataString and request.headers to verify payload structure
  • Monitor Business Rule execution by temporarily disabling all table-specific rules to isolate script-only behavior
  • Use REST API Explorer to test with minimal payloads, gradually adding complexity to isolate the failing component

Quick Reference

  • Maximum script execution time is 30 seconds before ServiceNow terminates the request with a timeout error
  • The request.body.dataString property is null for GET requests and empty POST requests, causing JSON.parse to fail without null checking
  • Response headers must be set before writing any response body data, or ServiceNow throws IllegalStateException
  • Path parameters in resource URLs like /api/custom/{id} are accessible via request.pathParams.id not query parameters
  • Scripts run with the authenticated user's session context, meaning timezone and language preferences affect date formatting and GlideRecord behavior
  • The sys_ws_definition table requires web_service_admin role to create or modify, but individual resources can be managed with rest_service role
  • Versioning requires creating entirely new API definitions; you cannot version individual resources within the same sys_ws_definition record
  • Query parameters with special characters require URL encoding; ServiceNow doesn't automatically decode them in request.queryParams
  • Cross-origin requests require explicit CORS headers in the script; ServiceNow doesn't add them automatically even with CORS rules configured
  • Scripts cannot access the session object directly; use gs.getSession() to read session variables, but writing session data from REST APIs is unreliable