Scripted REST API Development
SkillDev toolsComprehensive 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.
No other account needed.
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 definitionsadmin/application-scope- Scoped app API developmentsecurity/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
| Table | Purpose | Key Fields |
|---|---|---|
sys_ws_definition | API container | name, namespace, base_path, active |
sys_ws_operation | Individual endpoints | name, http_method, relative_path, script |
sys_script_include | Reusable logic | name, script, api_name |
sys_ws_api_header | Custom headers | name, 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 Type | Use Case | Configuration |
|---|---|---|
| Basic Auth | Service accounts | Enabled by default |
| OAuth 2.0 | Third-party apps | Configure OAuth provider |
| API Key | Simple integrations | Custom header validation |
| Session | Browser-based | Cookie-based auth |
| Mutual TLS | High security | Certificate 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:
| Field | Purpose | Recommendation |
|---|---|---|
namespace | URL segment, typically app scope | Use app scope (x_company) |
enforce_acl | Check ACLs on underlying tables | Set true for security |
is_versioned | Enable API versioning | Always true |
baseline_version | Default version if none specified | Start with v1 |
published | Make API discoverable | Set 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