ServiceNow's Scripted REST API lets you expose custom endpoints with full JavaScript control over request handling, business logic, and response formatting. That flexibility is powerful — and dangerous. An API that works correctly in testing can leak data, break under load, or become an attack surface if built without discipline.
These 10 practices will help you build Scripted REST APIs that are secure, maintainable, and production-ready from day one.
1. Validate Every Request Parameter
Never trust incoming data. Every query parameter, path variable, and request body field is a potential injection vector or source of unexpected behavior. Validate before processing.
(function process(/* RESTRequestAPI */ request, /* RESTResponseAPI */ response) {
var params = request.queryParams;
// Validate required params exist and are non-empty
if (!params.limit || isNaN(parseInt(params.limit, 10))) {
return badRequest(response, 'Missing or invalid "limit" parameter');
}
var limit = Math.min(parseInt(params.limit, 10), 100); // Cap at 100
var offset = parseInt(params.offset || '0', 10);
// Validate offset
if (offset < 0) {
return badRequest(response, 'Offset must be non-negative');
}
// Process with validated values...
})(request, response);
function badRequest(response, message) {
response.setStatus(400);
response.setBody({ error: message });
}
Apply allowlists for enum values, length limits for strings, and type checks for numbers. Reject early with clear error messages.
2. Use Basic Auth Over HTTPS — Never Plain HTTP
ServiceNow Scripted REST APIs should always be accessed over HTTPS. For internal or low-sensitivity integrations, Basic Auth is straightforward to implement. Store the username and password in SecureImportSet credentials or a protected system property — never hardcode them.
var auth = request.getHeader('Authorization');
if (!auth || !auth.startsWith('Basic ')) {
response.setStatus(401);
response.setHeader('WWW-Authenticate', 'Basic realm="SN"');
response.setBody({ error: 'Authentication required' });
return;
}
var decoded = new global.JSON().decode(
new global.CompatibilityUtils().base64Decode(auth.substring(6))
);
var creds = decoded.split(':', 2);
// Validate against stored credentials
For higher-security integrations, implement OAuth 2.0 or a custom token validation approach rather than Basic Auth. Consult the ServiceNow documentation for your specific release to confirm available authentication options. Never expose admin credentials to external API consumers.
3. Set Explicit Response Types and Status Codes
Ambiguous responses cause consumer confusion and broken integrations. Always set the HTTP status code explicitly and return a consistent response envelope.
var result = processIncidentRequest(request);
response.setStatus(result.statusCode || 200);
response.setContentType('application/json');
response.setBody(result.body);
Use standard status codes deliberately:
- 200 OK — success with data
- 201 Created — resource created (include the new resource URI in Location header)
- 400 Bad Request — invalid input
- 401 Unauthorized — missing or invalid auth
- 404 Not Found — resource doesn't exist
- 429 Too Many Requests — rate limit exceeded
- 500 Internal Server Error — unexpected server failure (never expose stack traces)
4. Return Consistent JSON Envelopes
Define a response envelope and use it everywhere. Consumers should always get the same structure:
function okResponse(data, total, limit, offset) {
return {
result: data,
meta: {
total: total,
limit: limit,
offset: offset,
hasMore: (offset + data.length) < total
}
};
}
function errorResponse(code, message, details) {
return {
error: {
code: code,
message: message,
details: details || null
}
};
}
Consistent envelopes make it trivial for consumers to parse success vs. error, handle pagination, and debug issues in production.
5. Implement Pagination from the Start
If your API returns lists, implement pagination from the beginning. Retrofit it later and you'll break existing consumers.
(function process(request, response) {
var limit = Math.min(parseInt(request.queryParams.limit || '20', 10), 100);
var offset = parseInt(request.queryParams.offset || '0', 10);
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.orderByDesc('sys_created_on');
gr.setRowLimit(limit);
gr.setWorkflow(false);
// For very large tables, use GlideAggregate for count instead of getRowCount()
// to avoid the overhead of a full SELECT COUNT(*) on every request
var total = gr.getRowCount();
gr.absoluteWindow(offset, limit);
var results = [];
while (gr.next()) {
results.push({
sys_id: gr.sys_id.toString(),
number: gr.number.toString(),
short_description: gr.short_description.toString(),
state: gr.state.getDisplayValue()
});
}
response.setBody(okResponse(results, total, limit, offset));
})(request, response);
Use getRowCount() cautiously — on very large tables prefer GlideAggregate with a separate count query to avoid the overhead of a full count on every request.
6. Handle Errors Gracefully — Never Leak Stack Traces
Unexpected errors should return a safe message to the consumer and log the full detail server-side. Never expose JavaScript stack traces, internal table names, or system paths.
try {
var result = riskyOperation();
response.setBody(okResponse(result));
response.setStatus(200);
} catch (e) {
gs.error('Scripted REST API error: ' + e.message + '\n' + e.stack);
response.setStatus(500);
response.setBody(errorResponse('INTERNAL_ERROR', 'An unexpected error occurred'));
}
Consider differentiating expected errors (business rule rejections) from unexpected errors (exceptions) — both return 500 to the consumer, but logging levels differ.
7. Log Request Metadata for Audit and Debugging
At minimum, log the endpoint, method, authenticated user, source IP, and response status for every request. Write this to the console or a custom audit table.
(function process(request, response) {
var startMs = GlideDateTime.getNumericSessionDateTime();
// Log request
gs.info('REST API: ' + request.method + ' ' + request.path +
' | user=' + request.getHeader('X-User-ID') +
' | ip=' + request.getHeader('X-Forwarded-For'));
// ... process request ...
var duration = GlideDateTime.getNumericSessionDateTime() - startMs;
gs.info('REST API response: ' + response.getStatusCode() +
' | duration_ms=' + duration);
})(request, response);
For production APIs, route these logs to a SIEM or log aggregation tool. This data is invaluable for debugging integration failures and identifying abuse patterns.
8. Use IntegrationHub Where Possible Before Going Custom
IntegrationHub provides pre-built, tested, and supported REST steps for common operations — SAP, Microsoft, Salesforce, and hundreds more. Before writing a custom Scripted REST API for a standard integration, check IntegrationHub's available endpoints.
Custom Scripted REST APIs shine for:
- Aggregating data from multiple ServiceNow tables
- Implementing custom business logic not available in standard actions
- Exposing domain-specific endpoints for a specific consumer
- Transforming data between ServiceNow and external formats
Use the right tool for the job. A custom Scripted REST API is not the answer to every integration need.
9. Document Your API with OpenAPI
Every Scripted REST API should come with a documented contract. Use the ServiceNow API Docs approach — create a Markdown or OpenAPI (Swagger) specification alongside the API definition and make it available to consumers.
At minimum, document:
- Endpoint and method (e.g.,
POST /api/sn-custom/incident/escalate) - Authentication — what headers are required
- Request body — field names, types, required vs. optional, example
- Response — status codes, body structure, example
- Error codes — what each error code means and how to resolve it
- Rate limits — requests per minute/hour if applicable
Undocumented APIs create confusion, generate support tickets, and get misused.
10. Test with Realistic Data and Failure Scenarios
Test beyond the happy path. A Scripted REST API that works with valid input can fail silently with empty arrays, null values, or concurrent requests.
Test these scenarios explicitly:
- Missing required parameters — confirm 400 response
- Malformed JSON body — confirm 400, not 500
- Invalid authentication — confirm 401, not 200 with empty body
- Very large result sets — confirm pagination works and no memory issues
- Concurrent requests — confirm no race conditions on shared state
- Timeout behavior — confirm external calls have timeouts and the API fails gracefully
Use Postman or curl locally, then validate against your production-like test environment before deploying.
Final Thoughts
Scripted REST APIs are one of ServiceNow's most powerful extensibility features. They let you build exactly the integration your consumers need — but that power comes with responsibility. Input validation, consistent responses, proper error handling, and audit logging are not optional extras for production APIs. They are the baseline.
Start with validation and consistent envelopes, add proper authentication, and build from there. Your consumers — and your on-call team — will thank you.