Scripted REST API Development

SkillDev tools

Comprehensive guide to creating, securing, and testing Scripted REST APIs in ServiceNow for custom integrations and external system connectivity

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Scripted REST API Development skill

What this skill tells your AI

The instructions your AI receives, as published by happy-technologies-llc/happy-platform-skills in skills/development/scripted-rest-apis/SKILL.md and read by ahel’s review.

Overview

This skill provides a comprehensive guide to developing custom Scripted REST APIs in ServiceNow. Scripted REST APIs allow you to:

  • Expose custom endpoints - Create purpose-built APIs beyond the standard Table API
  • Control data transformation - Shape request/response formats to match integration needs
  • Implement business logic - Execute complex operations in a single API call
  • Secure integrations - Apply fine-grained authentication and authorization
  • Support external systems - Provide interfaces for third-party system integration

When to use Scripted REST APIs:

  • Standard Table API doesn't meet requirements
  • Need custom business logic in the API layer
  • Require specific request/response formats
  • External systems need specialized endpoints
  • Need to aggregate data from multiple tables

Who should use this: Developers, integration specialists, and administrators building custom ServiceNow integrations.

Prerequisites

  • Roles: admin, web_service_admin, or scoped app developer
  • Permissions: Create/modify sys_ws_definition, sys_ws_operation
  • Knowledge: JavaScript, REST principles, HTTP methods, JSON
  • Environment: Development instance (never develop APIs directly in production)
  • Related Skills:
    • admin/update-set-management - Capture API definitions
    • admin/application-scope - Scoped app API development
    • security/acl-management - API security controls

Table Architecture

Understanding the underlying tables is essential for programmatic API creation.

Core Tables

sys_ws_definition (API Definition)
    |
    +-- sys_ws_operation (Resources/Endpoints)
            |
            +-- Request Headers (configured in operation)
            +-- Query Parameters (configured in operation)
            +-- Path Parameters (defined in relative_path)
            +-- Script (handles request/response)

Key Relationships

TablePurposeKey Fields
sys_ws_definitionAPI containername, namespace, base_path, active
sys_ws_operationIndividual endpointsname, http_method, relative_path, script
sys_script_includeReusable logicname, script, api_name
sys_ws_api_headerCustom headersname, api_id, direction

API URL Structure

https://<instance>.service-now.com/api/<namespace>/<api_name>/<version>/<resource>/<path_param>

Example:
https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/12345
    |                                |     |            |       |           |
    Instance                     Namespace  API Name  Version Resource  Path Param

Procedure

Phase 1: API Design

Before creating the API, plan the structure carefully.

Step 1.1: Define API Requirements

Document the API specification:

API Name: Customer Integration API
Namespace: x_company (scoped app) or now (global)
Version: v1
Base Path: /api/x_company/customer_api

Resources:
- GET /customers - List all customers (with pagination)
- GET /customers/{id} - Get specific customer
- POST /customers - Create new customer
- PUT /customers/{id} - Update customer
- DELETE /customers/{id} - Delete customer
- POST /customers/{id}/orders - Create order for customer
Step 1.2: Plan Authentication
Auth TypeUse CaseConfiguration
Basic AuthService accountsEnabled by default
OAuth 2.0Third-party appsConfigure OAuth provider
API KeySimple integrationsCustom header validation
SessionBrowser-basedCookie-based auth
Mutual TLSHigh securityCertificate validation

Phase 2: Create API Definition

Step 2.1: Set Context (Scoped App)

If creating in a scoped application:

Using MCP:

Tool: SN-Set-Current-Application
Parameters:
  app_sys_id: [your_app_sys_id]
Tool: SN-Set-Update-Set
Parameters:
  update_set_sys_id: [your_update_set_sys_id]
Step 2.2: Create API Definition

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_definition
  data:
    name: Customer Integration API
    short_description: API for customer management and integration
    namespace: x_company
    doc_link: https://docs.company.com/api/customers
    active: true
    enforce_acl: true
    is_versioned: true
    baseline_version: v1
    published: false

Key Fields Explained:

FieldPurposeRecommendation
namespaceURL segment, typically app scopeUse app scope (x_company)
enforce_aclCheck ACLs on underlying tablesSet true for security
is_versionedEnable API versioningAlways true
baseline_versionDefault version if none specifiedStart with v1
publishedMake API discoverableSet false during development

Save the sys_id: api_definition_sys_id

Step 2.3: Verify API Creation
Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_definition
  query: name=Customer Integration API
  fields: sys_id,name,namespace,active,service_address

The service_address field shows the full API URL.

Phase 3: Create Resources (Operations)

Step 3.1: GET Collection Resource (List)

Create an endpoint to list customers with pagination.

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: List Customers
    web_service_definition: [api_definition_sys_id]
    http_method: GET
    relative_path: /customers
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // Get query parameters for pagination and filtering
        var limit = parseInt(request.queryParams.limit) || 20;
        var offset = parseInt(request.queryParams.offset) || 0;
        var active = request.queryParams.active;
        var search = request.queryParams.q;

        // Validate limit
        if (limit > 100) limit = 100;
        if (limit < 1) limit = 20;

        // Build query
        var gr = new GlideRecord('customer');

        if (active !== undefined) {
          gr.addQuery('active', active === 'true');
        }

        if (search) {
          var qc = gr.addQuery('name', 'CONTAINS', search);
          qc.addOrCondition('email', 'CONTAINS', search);
          qc.addOrCondition('account_number', 'CONTAINS', search);
        }

        // Get total count before pagination
        var countGR = new GlideAggregate('customer');
        countGR.addAggregate('COUNT');
        if (active !== undefined) {
          countGR.addQuery('active', active === 'true');
        }
        countGR.query();
        var totalCount = 0;
        if (countGR.next()) {
          totalCount = parseInt(countGR.getAggregate('COUNT'));
        }

        // Apply pagination
        gr.orderBy('name');
        gr.chooseWindow(offset, offset + limit);
        gr.query();

        // Build response
        var customers = [];
        while (gr.next()) {
          customers.push({
            sys_id: gr.getUniqueValue(),
            name: gr.getValue('name'),
            email: gr.getValue('email'),
            account_number: gr.getValue('account_number'),
            active: gr.getValue('active') === 'true',
            created: gr.getValue('sys_created_on'),
            updated: gr.getValue('sys_updated_on')
          });
        }

        // Set response
        response.setStatus(200);
        response.setBody({
          result: customers,
          meta: {
            total: totalCount,
            limit: limit,
            offset: offset,
            has_more: (offset + limit) < totalCount
          }
        });

      })(request, response);
    short_description: Retrieve list of customers with pagination
    requires_acl_authorization: true
    requires_authentication: true
    requires_snc_internal_role: false

Save the sys_id: list_customers_operation_id

Step 3.2: GET Single Resource

Create an endpoint to retrieve a specific customer.

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Get Customer
    web_service_definition: [api_definition_sys_id]
    http_method: GET
    relative_path: /customers/{id}
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // Get path parameter
        var customerId = request.pathParams.id;

        // Validate input
        if (!customerId) {
          response.setStatus(400);
          response.setBody({
            error: {
              message: 'Customer ID is required',
              code: 'MISSING_PARAMETER'
            }
          });
          return;
        }

        // Query customer
        var gr = new GlideRecord('customer');

        // Support both sys_id and account_number lookup
        if (customerId.length === 32) {
          gr.get(customerId);
        } else {
          gr.addQuery('account_number', customerId);
          gr.query();
          gr.next();
        }

        if (!gr.isValidRecord()) {
          response.setStatus(404);
          response.setBody({
            error: {
              message: 'Customer not found',
              code: 'NOT_FOUND',
              detail: 'No customer exists with ID: ' + customerId
            }
          });
          return;
        }

        // Check ACL (optional if enforce_acl is true on definition)
        if (!gr.canRead()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Access denied',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        // Build detailed response
        var customer = {
          sys_id: gr.getUniqueValue(),
          name: gr.getValue('name'),
          email: gr.getValue('email'),
          phone: gr.getValue('phone'),
          account_number: gr.getValue('account_number'),
          address: {
            street: gr.getValue('street'),
            city: gr.getValue('city'),
            state: gr.getValue('state'),
            zip: gr.getValue('zip'),
            country: gr.getValue('country')
          },
          contacts: [],
          active: gr.getValue('active') === 'true',
          created: gr.getValue('sys_created_on'),
          updated: gr.getValue('sys_updated_on'),
          created_by: gr.getDisplayValue('sys_created_by'),
          updated_by: gr.getDisplayValue('sys_updated_by')
        };

        // Get related contacts
        var contacts = new GlideRecord('customer_contact');
        contacts.addQuery('customer', gr.getUniqueValue());
        contacts.query();
        while (contacts.next()) {
          customer.contacts.push({
            sys_id: contacts.getUniqueValue(),
            name: contacts.getValue('name'),
            email: contacts.getValue('email'),
            phone: contacts.getValue('phone'),
            primary: contacts.getValue('primary') === 'true'
          });
        }

        response.setStatus(200);
        response.setBody({ result: customer });

      })(request, response);
    short_description: Retrieve a specific customer by ID
    requires_acl_authorization: true
    requires_authentication: true
Step 3.3: POST Resource (Create)

Create an endpoint to create a new customer.

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Create Customer
    web_service_definition: [api_definition_sys_id]
    http_method: POST
    relative_path: /customers
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // Parse request body
        var body = request.body.data;

        // Validate required fields
        var errors = [];
        if (!body.name) errors.push('name is required');
        if (!body.email) errors.push('email is required');

        // Validate email format
        if (body.email && !body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
          errors.push('email format is invalid');
        }

        if (errors.length > 0) {
          response.setStatus(400);
          response.setBody({
            error: {
              message: 'Validation failed',
              code: 'VALIDATION_ERROR',
              details: errors
            }
          });
          return;
        }

        // Check for duplicate email
        var existing = new GlideRecord('customer');
        existing.addQuery('email', body.email);
        existing.query();
        if (existing.next()) {
          response.setStatus(409);
          response.setBody({
            error: {
              message: 'Customer with this email already exists',
              code: 'DUPLICATE_ERROR',
              existing_id: existing.getUniqueValue()
            }
          });
          return;
        }

        // Create customer
        var gr = new GlideRecord('customer');
        gr.initialize();

        // Required fields
        gr.setValue('name', body.name);
        gr.setValue('email', body.email);

        // Optional fields
        if (body.phone) gr.setValue('phone', body.phone);
        if (body.account_number) gr.setValue('account_number', body.account_number);
        if (body.address) {
          if (body.address.street) gr.setValue('street', body.address.street);
          if (body.address.city) gr.setValue('city', body.address.city);
          if (body.address.state) gr.setValue('state', body.address.state);
          if (body.address.zip) gr.setValue('zip', body.address.zip);
          if (body.address.country) gr.setValue('country', body.address.country);
        }

        gr.setValue('active', true);

        // Check create permission
        if (!gr.canCreate()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Permission denied to create customer',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        var sysId = gr.insert();

        if (sysId) {
          // Set Location header for created resource
          response.setHeader('Location', request.url + '/' + sysId);
          response.setStatus(201);
          response.setBody({
            result: {
              sys_id: sysId,
              message: 'Customer created successfully'
            }
          });
        } else {
          response.setStatus(500);
          response.setBody({
            error: {
              message: 'Failed to create customer',
              code: 'INTERNAL_ERROR'
            }
          });
        }

      })(request, response);
    short_description: Create a new customer
    requires_acl_authorization: true
    requires_authentication: true
Step 3.4: PUT Resource (Update)

Create an endpoint to update an existing customer.

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Update Customer
    web_service_definition: [api_definition_sys_id]
    http_method: PUT
    relative_path: /customers/{id}
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        var customerId = request.pathParams.id;
        var body = request.body.data;

        // Find customer
        var gr = new GlideRecord('customer');
        if (!gr.get(customerId)) {
          response.setStatus(404);
          response.setBody({
            error: {
              message: 'Customer not found',
              code: 'NOT_FOUND'
            }
          });
          return;
        }

        // Check update permission
        if (!gr.canWrite()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Permission denied to update customer',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        // Validate email if being changed
        if (body.email && body.email !== gr.getValue('email')) {
          if (!body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
            response.setStatus(400);
            response.setBody({
              error: {
                message: 'Invalid email format',
                code: 'VALIDATION_ERROR'
              }
            });
            return;
          }

          // Check for duplicate
          var existing = new GlideRecord('customer');
          existing.addQuery('email', body.email);
          existing.addQuery('sys_id', '!=', customerId);
          existing.query();
          if (existing.next()) {
            response.setStatus(409);
            response.setBody({
              error: {
                message: 'Another customer with this email exists',
                code: 'DUPLICATE_ERROR'
              }
            });
            return;
          }
        }

        // Update fields
        var updatedFields = [];

        if (body.name !== undefined) {
          gr.setValue('name', body.name);
          updatedFields.push('name');
        }
        if (body.email !== undefined) {
          gr.setValue('email', body.email);
          updatedFields.push('email');
        }
        if (body.phone !== undefined) {
          gr.setValue('phone', body.phone);
          updatedFields.push('phone');
        }
        if (body.active !== undefined) {
          gr.setValue('active', body.active);
          updatedFields.push('active');
        }
        if (body.address) {
          if (body.address.street !== undefined) gr.setValue('street', body.address.street);
          if (body.address.city !== undefined) gr.setValue('city', body.address.city);
          if (body.address.state !== undefined) gr.setValue('state', body.address.state);
          if (body.address.zip !== undefined) gr.setValue('zip', body.address.zip);
          if (body.address.country !== undefined) gr.setValue('country', body.address.country);
          updatedFields.push('address');
        }

        gr.update();

        response.setStatus(200);
        response.setBody({
          result: {
            sys_id: customerId,
            message: 'Customer updated successfully',
            updated_fields: updatedFields
          }
        });

      })(request, response);
    short_description: Update an existing customer
    requires_acl_authorization: true
    requires_authentication: true
Step 3.5: DELETE Resource

Create an endpoint to delete a customer.

Using MCP:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Delete Customer
    web_service_definition: [api_definition_sys_id]
    http_method: DELETE
    relative_path: /customers/{id}
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        var customerId = request.pathParams.id;

        // Find customer
        var gr = new GlideRecord('customer');
        if (!gr.get(customerId)) {
          response.setStatus(404);
          response.setBody({
            error: {
              message: 'Customer not found',
              code: 'NOT_FOUND'
            }
          });
          return;
        }

        // Check delete permission
        if (!gr.canDelete()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Permission denied to delete customer',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        // Soft delete vs hard delete decision
        // Option 1: Soft delete (preferred)
        gr.setValue('active', false);
        gr.update();

        // Option 2: Hard delete (use with caution)
        // gr.deleteRecord();

        response.setStatus(200);
        response.setBody({
          result: {
            sys_id: customerId,
            message: 'Customer deactivated successfully'
          }
        });

        // Alternative: Return 204 No Content for true DELETE
        // response.setStatus(204);

      })(request, response);
    short_description: Delete (deactivate) a customer
    requires_acl_authorization: true
    requires_authentication: true

Phase 4: Authentication and Security

Step 4.1: Configure API-Level Authentication

The API definition controls authentication requirements:

Tool: SN-Update-Record
Parameters:
  table_name: sys_ws_definition
  sys_id: [api_definition_sys_id]
  data:
    requires_authentication: true
    enforce_acl: true
    acl_failure_result: UNAUTHORIZED
Step 4.2: Implement API Key Authentication

For simple integrations, implement API key validation:

Create API Key Script Include:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: CustomerAPIAuth
    script: |
      var CustomerAPIAuth = Class.create();
      CustomerAPIAuth.prototype = {
        initialize: function() {
          this.API_KEY_HEADER = 'X-API-Key';
          this.API_KEY_TABLE = 'x_company_api_keys';
        },

        validateApiKey: function(request) {
          var apiKey = request.getHeader(this.API_KEY_HEADER);

          if (!apiKey) {
            return {
              valid: false,
              error: 'API key required',
              code: 'MISSING_API_KEY'
            };
          }

          // Look up API key
          var keyRecord = new GlideRecord(this.API_KEY_TABLE);
          keyRecord.addQuery('key_value', apiKey);
          keyRecord.addQuery('active', true);
          keyRecord.addQuery('expires', '>', new GlideDateTime());
          keyRecord.query();

          if (!keyRecord.next()) {
            return {
              valid: false,
              error: 'Invalid or expired API key',
              code: 'INVALID_API_KEY'
            };
          }

          // Update last used timestamp
          keyRecord.setValue('last_used', new GlideDateTime());
          keyRecord.update();

          return {
            valid: true,
            client_id: keyRecord.getValue('client_id'),
            permissions: keyRecord.getValue('permissions').split(','),
            rate_limit: parseInt(keyRecord.getValue('rate_limit')) || 1000
          };
        },

        type: 'CustomerAPIAuth'
      };
    api_name: CustomerAPIAuth
    active: true
    access: public

Use in API Operation:

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  // Validate API key
  var auth = new CustomerAPIAuth();
  var authResult = auth.validateApiKey(request);

  if (!authResult.valid) {
    response.setStatus(401);
    response.setBody({
      error: {
        message: authResult.error,
        code: authResult.code
      }
    });
    return;
  }

  // Check permission
  if (authResult.permissions.indexOf('read:customers') === -1) {
    response.setStatus(403);
    response.setBody({
      error: {
        message: 'Insufficient permissions',
        code: 'FORBIDDEN',
        required: 'read:customers'
      }
    });
    return;
  }

  // Continue with request processing...

})(request, response);
Step 4.3: Implement Rate Limiting

Add rate limiting to protect the API:

Create Rate Limiter Script Include:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: APIRateLimiter
    script: |
      var APIRateLimiter = Class.create();
      APIRateLimiter.prototype = {
        initialize: function(clientId, rateLimit) {
          this.clientId = clientId;
          this.rateLimit = rateLimit || 100;  // requests per minute
          this.CACHE_PREFIX = 'api_rate_';
        },

        isAllowed: function() {
          var cacheKey = this.CACHE_PREFIX + this.clientId;
          var cache = new GlideSysCache('APIRateLimit');

          // Get current count
          var countStr = cache.get(cacheKey);
          var count = countStr ? parseInt(countStr) : 0;

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
37
Forks
13
Last commit
Jul 2026
Advanced
Catalog kind
skill
Gateway key
scripted-rest-apis
Source
github.com/happy-technologies-llc/happy-platform-skills