How to Create a Scripted REST API in ServiceNow

The Table API covers most standard data access scenarios, but there are times when you need a completely custom REST endpoint — your own URL structure, your own request format, your own business logic. Scripted REST APIs let you build that. This guide walks through creating a Scripted REST API from scratch, handling requests properly, securing the endpoint, and testing it before production.

ServiceNow's built-in Table API is powerful and sufficient for most standard integration needs — reading records, creating records, updating fields. But it has limitations. The URL structure is fixed. The response format is fixed. Every call maps directly to a table. When your integration requires something beyond that — a single endpoint that creates an incident, assigns it, notifies a team, and returns a custom response — the Table API cannot do it in one call.

Scripted REST APIs solve this. They let you define custom endpoints with your own URL paths, handle incoming requests with server-side JavaScript, execute any business logic you need, and return any response format you choose. External systems see a clean, purposeful API — they have no knowledge of ServiceNow's internal table structure.

When to Use a Scripted REST API vs the Table API

Use the Table API when the external system needs standard CRUD access to ServiceNow records. Use a Scripted REST API when:

  • You need to execute multiple operations in a single call (create + assign + notify)
  • You want to hide ServiceNow's internal table structure from the calling system
  • You need a custom URL convention that matches the external system's integration standards
  • You need a custom response format different from the Table API's standard JSON envelope
  • You need input validation or data transformation before writing to ServiceNow tables

Step 1 — Create the API Record

Navigate to System Web Services > Scripted REST APIs and click New. Fill in the following fields:

  • Name: A human-readable name — for example, "Incident Manager API"
  • API ID: A machine-readable identifier used in the URL — for example, incident_manager. Use lowercase letters, numbers, and underscores only. No spaces.
  • Default Accept: application/json
  • Default Content Type: application/json

Save the record. ServiceNow assigns a namespace based on your application scope. If you are working in a scoped application with prefix x_myco, your API base path will be:

/api/x_myco/incident_manager

If you are working in the global scope (not recommended for new development), the namespace will be global. Always develop Scripted REST APIs inside a scoped application.

Step 2 — Add a Resource

A resource is an individual endpoint path within your API. In the REST API record, scroll to the Resources related list and click New.

For an incident creation endpoint, configure:

  • Name: Create Incident
  • Relative Path: /create — this appends to the API base path
  • HTTP Method: POST
  • Requires Authentication: checked
  • Requires ACL Authorization: checked

The full endpoint URL for this resource will be:

POST https://<instance>.service-now.com/api/x_myco/incident_manager/create

You can add multiple resources to the same API record — one for create, one for update, one for status lookup. Each resource has its own HTTP method, path, and script.

Step 3 — Write the Script

The script for each resource is a JavaScript function that receives a request object and a response object. Access the incoming payload through request.body.data, read query parameters through request.queryParams, and read path parameters through request.pathParams.

Here is a complete incident creation script with input validation and error handling:

(function process(request, response) {

    var body = request.body.data;

    // --- Input validation ---
    if (!body) {
        response.setStatus(400);
        response.setBody({ error: 'Request body is required' });
        return;
    }
    if (!body.short_description) {
        response.setStatus(400);
        response.setBody({ error: 'short_description is required' });
        return;
    }
    if (!body.caller_id) {
        response.setStatus(400);
        response.setBody({ error: 'caller_id is required' });
        return;
    }

    // --- Resolve caller by email ---
    var grUser = new GlideRecord('sys_user');
    grUser.addQuery('email', body.caller_id);
    grUser.setLimit(1);
    grUser.query();

    if (!grUser.next()) {
        response.setStatus(404);
        response.setBody({ error: 'Caller not found: ' + body.caller_id });
        return;
    }

    // --- Create the incident ---
    try {
        var gr = new GlideRecord('incident');
        gr.initialize();
        gr.short_description = body.short_description;
        gr.caller_id          = grUser.getUniqueValue();
        gr.category           = body.category    || 'inquiry';
        gr.subcategory        = body.subcategory || '';
        gr.priority           = body.priority    || 3;
        gr.description        = body.description || '';

        var sysId = gr.insert();

        if (!sysId) {
            response.setStatus(500);
            response.setBody({ error: 'Failed to create incident' });
            return;
        }

        // Reload to get generated fields (number, state)
        gr.get(sysId);

        response.setStatus(201);
        response.setBody({
            sys_id:            sysId,
            number:            gr.getValue('number'),
            short_description: gr.getValue('short_description'),
            state:             gr.getDisplayValue('state'),
            priority:          gr.getDisplayValue('priority'),
            created_on:        gr.getValue('sys_created_on')
        });

    } catch (e) {
        gs.error('IncidentManagerAPI create error: ' + e.message);
        response.setStatus(500);
        response.setBody({ error: 'Internal server error', detail: e.message });
    }

})(request, response);

This script validates required fields, resolves the caller by email address (so the calling system does not need to know ServiceNow sys_ids), creates the incident with sensible defaults for optional fields, and returns a structured response with the created record's key details.

Step 4 — Add Path Parameters for GET Endpoints

For endpoints that retrieve a specific record, use path parameters. In the resource Relative Path, define:

/status/{incidentNumber}

The {incidentNumber} segment is a path parameter. Access it in the script:

(function process(request, response) {

    var incidentNumber = request.pathParams.incidentNumber;

    if (!incidentNumber) {
        response.setStatus(400);
        response.setBody({ error: 'Incident number is required' });
        return;
    }

    var gr = new GlideRecord('incident');
    gr.addQuery('number', incidentNumber);
    gr.setLimit(1);
    gr.query();

    if (!gr.next()) {
        response.setStatus(404);
        response.setBody({ error: 'Incident not found: ' + incidentNumber });
        return;
    }

    response.setStatus(200);
    response.setBody({
        number:            gr.getValue('number'),
        state:             gr.getDisplayValue('state'),
        priority:          gr.getDisplayValue('priority'),
        assigned_to:       gr.assigned_to.getDisplayValue(),
        short_description: gr.getValue('short_description'),
        resolved_at:       gr.getValue('resolved_at')
    });

})(request, response);

The full URL for this resource would be: GET /api/x_myco/incident_manager/status/INC0012345

Step 5 — Secure the Endpoint

Every Scripted REST API endpoint requires authentication. The "Requires Authentication" checkbox on the resource record enforces this — any call without valid credentials returns 401.

For the authentication method, configure OAuth 2.0 Client Credentials for production integrations. Navigate to System OAuth > Application Registry, create an OAuth endpoint for external clients, and share the client_id and client_secret with the external system team.

You can also add an ACL to the Scripted REST API. Under System Security > Access Control, create an ACL with Operation type "REST_Endpoint" and the path pattern matching your API. Assign the required role. Only service accounts with that role can call the endpoint.

Never test with or document admin credentials. Create a dedicated service account with the minimum roles required and test the endpoint using that account. Admin accounts bypass ACLs — if you test as admin you will miss permission issues that affect the production service account.

Step 6 — Test with the REST API Explorer

Before sharing the endpoint URL with the external system team, test it using ServiceNow's built-in REST API Explorer. Navigate to System Web Services > REST API Explorer.

Select your namespace and API from the dropdowns. The Explorer automatically populates the endpoint URL and provides an interactive request builder where you can set headers, body, and path parameters. Click Send to execute the request and inspect the response — status code, response body, and response headers — in real time.

The REST API Explorer is also the fastest way to generate sample request code. After a successful test, click the "Code" tab to see the equivalent request in curl, Python, JavaScript, or Java. Copy this to your integration specification so the external system team has a working example to build from.

Step 7 — Version Your API

If external systems are already integrated with your API and you need to make breaking changes — adding required fields, changing the response structure — version the API rather than modifying the existing one.

Create a new API record with an incremented version suffix in the API ID: incident_manager_v2. This gives the new endpoint a different URL, allowing existing integrations to continue using the v1 endpoint while new integrations use v2. Communicate a deprecation timeline for v1 and give integration partners time to migrate.

HTTP Status Codes to Use

Use standard HTTP status codes consistently in your Scripted REST API responses:

  • 200 OK — successful GET or PATCH
  • 201 Created — successful POST that created a record
  • 400 Bad Request — missing required field, invalid format, validation failure
  • 401 Unauthorized — handled automatically by ServiceNow for missing auth
  • 403 Forbidden — authenticated but lacks permission
  • 404 Not Found — referenced record does not exist
  • 500 Internal Server Error — unhandled exception (always pair with error logging)

External system developers rely on status codes to implement error handling logic. Returning 200 with an error message in the body forces them to parse every response looking for errors. Use the correct status code and they can handle errors without inspecting the body.

Related:

ServiceNow Integrations — Complete Reference Guide

Scripted REST APIs, Table API, OAuth 2.0, MID Server, IntegrationHub — plus 50 integration interview Q&As. The complete integration reference for ServiceNow professionals.

Get the Guide →
← Back to all posts