Scripted REST APIs in ServiceNow: Build Custom Endpoints From Scratch

The Table API covers most data access scenarios. But when you need custom business logic, aggregated responses, or specific API contracts, Scripted REST APIs let you build custom HTTP endpoints directly on the ServiceNow platform. This guide covers when to build them, creating resources, request and response handling, path parameters, authentication, and production patterns. See our guide on a complete step-by-step walkthrough.

When to build a Scripted REST API

  • You need to execute business logic as part of the API call — not just read/write data
  • You need to aggregate data from multiple tables into one response
  • An external system expects a specific API contract that the Table API does not match
  • You need custom input validation or transformation before writing to ServiceNow tables
  • You want to expose ServiceNow functionality without exposing direct table access

Creating a Scripted REST API

Navigate to System Web Services > Scripted REST APIs > New. Key fields:

  • 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 of Resources — individual endpoints with their own URL path and HTTP method scripts:

// API: /api/nowspec/incident_metrics
// Resources:
//   GET  /summary          → overall metrics
//   GET  /by_group/{id}    → metrics for specific group
//   POST /acknowledge      → mark an incident acknowledged

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 parameters: /api/nowspec/incidents?priority=1&limit=50
var priority = request.queryParams.priority;
var limit = parseInt(request.queryParams.limit) || 25;

GET endpoint example

(function process(request, response) {
    var priority = request.queryParams.priority || '1';
    var limit = parseInt(request.queryParams.limit) || 25;

    var ga = new GlideAggregate('incident');
    ga.addEncodedQuery('active=true^priority=' + priority);
    ga.addAggregate('COUNT', 'assignment_group');
    ga.groupBy('assignment_group');
    ga.orderByAggregateDesc('COUNT', 'assignment_group');
    ga.setLimit(limit);
    ga.query();

    var results = [];
    while (ga.next()) {
        results.push({
            group: ga.assignment_group.getDisplayValue(),
            count: parseInt(ga.getAggregate('COUNT', 'assignment_group'))
        });
    }

    response.setStatus(200);
    response.setContentType('application/json');
    response.setBody(JSON.stringify({ result: results, total: results.length }));

})(request, response);

POST endpoint with input validation

(function process(request, response) {
    var body = {};
    try {
        body = JSON.parse(request.body.dataString);
    } catch (e) {
        response.setStatus(400);
        response.setBody(JSON.stringify({ error: 'Invalid JSON body' }));
        return;
    }

    if (!body.short_description) {
        response.setStatus(422);
        response.setBody(JSON.stringify({ error: 'short_description is required' }));
        return;
    }

    var gr = new GlideRecord('incident');
    gr.setValue('short_description', body.short_description);
    gr.setValue('category', body.category || 'software');
    gr.setValue('urgency', body.urgency || '3');
    var sys_id = gr.insert();

    response.setStatus(201);
    response.setBody(JSON.stringify({
        result: { sys_id: sys_id, number: gr.getValue('number') }
    }));

})(request, response);

Authentication and ACLs

Scripted REST APIs respect the same ACLs as the Table API. The user whose credentials are used for the API call must have access to the tables the script interacts with. Add custom role checks inside the script for additional access control:

if (!gs.hasRole('my_api_user')) {
    response.setStatus(403);
    response.setBody(JSON.stringify({ error: 'Access denied' }));
    return;
}

Related: REST API complete guide · Table API reference · GlideAggregate · OAuth 2.0

Creating a Scripted REST API resource

Navigate to Scripted REST APIs, create a new API record with a name and API namespace (e.g. "incident_api" under "x_myapp"). Add a Resource to define individual endpoints. Each resource has a method (GET, POST, PUT, DELETE), a relative path, and a Script that runs when the endpoint is called. The script has access to the request object (headers, query parameters, path parameters, body) and the response object (status code, body, headers):

// GET /api/x_myapp/incident_api/incidents/{sys_id}
(function process(request, response) {
    var sysId = request.pathParams.sys_id;
    
    if (!sysId || sysId.length !== 32) {
        response.setStatus(400);
        response.setBody({ error: 'Invalid sys_id' });
        return;
    }
    
    var gr = new GlideRecord('incident');
    if (!gr.get(sysId)) {
        response.setStatus(404);
        response.setBody({ error: 'Incident not found' });
        return;
    }
    
    // Check caller has read access
    if (!gr.canRead()) {
        response.setStatus(403);
        response.setBody({ error: 'Access denied' });
        return;
    }
    
    response.setStatus(200);
    response.setBody({
        number: gr.getValue('number'),
        short_description: gr.getValue('short_description'),
        state: gr.getDisplayValue('state'),
        priority: gr.getDisplayValue('priority'),
        assigned_to: gr.getDisplayValue('assigned_to')
    });
})(request, response);

Authentication for Scripted REST APIs

Scripted REST APIs use the same authentication mechanisms as the Table API — Basic Auth, OAuth 2.0, or mutual TLS. The calling user's ACLs apply to every query the script makes — if the caller does not have read access to an incident record, gr.get() returns false and the script should return 404 (not 403 — do not reveal whether a record exists to unauthorised callers). Design your APIs with the principle of least privilege: create a dedicated integration user with only the roles needed for that API's operations.

Related: REST API overview · Full implementation guide · Table API · OAuth 2.0 · Credential Aliases · GlideRecord performance

Scripted REST API versioning

ServiceNow supports API versioning on Scripted REST APIs. Create multiple versions of the same API (v1, v2) on the same API record — each version can have different resources with different logic. This lets you evolve your API without breaking existing callers: keep v1 running for existing integrations, build v2 with the new functionality, and migrate callers to v2 on their own timeline. Navigate to the Scripted REST API record and add a new API Version record for each version. The URL structure: /api/x_myapp/my_api/v1/resource vs /api/x_myapp/my_api/v2/resource. Versioning is a discipline worth building from your first Scripted REST API — retrofitting it after callers have hard-coded v1 URLs into their integration code is painful and often requires a lengthy deprecation period.

Testing with the REST API Explorer

The ServiceNow REST API Explorer (search "REST API Explorer" in the navigator) lets you test any REST API including your Scripted REST APIs directly from the browser, with authentication handled automatically. Select your API and version, choose a resource, fill in path and query parameters, and click "Send." The response appears inline with status code and body — faster than setting up Postman or curl for initial development testing. For regression testing after changes, export a Postman collection that covers success paths and common error paths (missing required parameter, record not found, unauthorised access), and run it against a development instance after any modification. The REST API Explorer is best for quick validation during development; external tools like Postman give more control over authentication scenarios and error simulation for thorough pre-production testing. The Script Debugger can also be attached to Scripted REST API executions to step through the resource script directly when test results don't match expectations.

Error Handling and Consistent Response Structure

Production Scripted REST APIs need a consistent error response format so that calling systems can parse failures reliably. Returning a plain string error for one failure and a JSON object for another forces consuming systems to handle multiple formats. Define a standard error envelope upfront: {"error": {"code": "RECORD_NOT_FOUND", "message": "Incident INC0012345 does not exist"}} and use it for every non-success response. Wrap the entire resource script body in a try-catch to guarantee that unexpected JavaScript errors return a structured 500 response rather than an HTML error page. The HTTP status codes guide covers the standard error code semantics to use in your error envelope.

Rate Limiting and Abuse Prevention

ServiceNow does not natively rate-limit calls to Scripted REST APIs — your script is responsible for implementing any necessary throttling. For APIs that trigger expensive operations (large GlideRecord queries, complex transforms), consider tracking call frequency per integration user in a custom table and returning 429 responses when a threshold is exceeded. More commonly, the protection is at the integration credential level: dedicated service account credentials for each calling system allow per-system monitoring in the transaction log, making it easy to identify and address unusually high call volumes. The GlideAggregate COUNT pattern against the transaction log provides the query needed to build call volume monitoring.

Documentation and API Contracts

A Scripted REST API without documentation is a maintenance liability. ServiceNow's API Documentation feature auto-generates documentation from your resource definitions — use it, but supplement it with human-written notes for each endpoint explaining the business context, expected input examples, error scenarios, and any important notes about rate limits or data freshness. For APIs consumed by external teams or vendors, treat the documented API contract as a formal commitment: a change that breaks the contract requires advance notice and a versioning increment. Store API documentation in a location that integration partners can access without ServiceNow credentials — a Confluence page or internal API portal is more accessible than the ServiceNow API Explorer for external consumers. The building guide covers the technical implementation; documentation is what makes it maintainable.

Versioning Strategy in Practice

Choosing the right versioning granularity requires balancing consumer convenience against maintenance burden. Too fine-grained versioning (a new version for every change) fragments consumers across many versions that all need to be maintained. Too coarse versioning (keeping a single version and making breaking changes) forces all consumers to update simultaneously, which is operationally impractical in large organisations. A practical middle ground: version the API at the major capability level (v1, v2, v3), where a new version indicates a significant redesign rather than incremental changes. Within a version, make only additive changes — new optional fields, new optional parameters, new endpoints. Use the standard versioning path format consistently so consuming systems can reliably construct API URLs across your application's endpoints.

The complete integrations reference

The NowSpectrum Integrations Guide covers Scripted REST APIs, Table API, OAuth 2.0, MID Server, and 50 interview Q&As.

Get the Integrations Guide →
← Back to all posts