API Design
Auto-Generating REST APIs from Configuration Records
Building a framework that reads from a custom table and dynamically registers Scripted REST Resources at runtime — no code deployments required.
The undocumented corners of the platform. Production-proven patterns in API design, integration architecture, authentication engineering, and upgrade governance.
// API Registry table: u_api_registry
// Fields: u_active, u_path, u_handler, u_auth_type, u_acl_role
// Called from a Script Include on activation
function registerAPIEndpoints() {
var registry = new GlideRecord('u_api_registry');
registry.addQuery('u_active', true);
registry.query();
while (registry.next()) {
var path = registry.u_path.toString();
var handler = registry.u_handler.toString();
var authType= registry.u_auth_type.toString();
var aclRole = registry.u_acl_role.toString();
createScriptedResource(path, handler, authType, aclRole);
}
}
function createScriptedResource(path, handler, authType, aclRole) {
// Check if resource already exists
var existing = new GlideRecord('sys_ws_endpoint');
existing.addQuery('relative_path', path);
existing.query();
if (existing.next()) return; // already registered
var endpoint = new GlideRecord('sys_ws_endpoint');
endpoint.newRecord();
endpoint.setValue('relative_path', path);
endpoint.setValue('script_include', handler);
endpoint.setValue('requires_authentication', authType !== 'none');
endpoint.setValue('required_role', aclRole);
endpoint.insert();
gs.log('API endpoint registered: ' + path);
}
// Pre-requisites:
// - Bearer token retrieved from credential store or token endpoint
// - mTLS alias configured in Connection & Credential Aliases
// - HMAC secret stored in sys_properties (encrypted)
function callSecureEndpoint(endpoint, payload) {
var r = new sn_ws.RESTMessageV2();
r.setEndpoint(endpoint);
r.setHttpMethod('POST');
r.setRequestBody(JSON.stringify(payload));
r.setHttpHeader('Content-Type', 'application/json');
// Layer 1: Bearer token
var token = getBearerToken(); // your token retrieval logic
r.setHttpHeader('Authorization', 'Bearer ' + token);
// Layer 2: HMAC-signed request header
var secret = gs.getProperty('u_api_hmac_secret');
var timestamp = new GlideDateTime().getNumericValue().toString();
var signature = computeHMAC(
JSON.stringify(payload) + timestamp, secret
);
r.setHttpHeader('X-Timestamp', timestamp);
r.setHttpHeader('X-Signature', signature);
// Layer 3: mTLS via MID Server keystore alias
r.setMutualAuth('u_mtls_client_alias');
var response = r.execute();
if (response.getStatusCode() !== 200) {
gs.logError('Secure endpoint call failed: '
+ response.getStatusCode()
+ ' ' + response.getBody());
return null;
}
return JSON.parse(response.getBody());
}
function computeHMAC(message, secret) {
// Use GlideDigest for HMAC-SHA256
var digest = new GlideDigest();
return digest.getSHA256HMACBase64(secret, message);
}
setMutualAuth() call references a Connection & Credential Alias configured with the client certificate — the MID Server handles the TLS handshake. This combination is required by some high-security financial and government integrations that won't accept any single auth mechanism alone.// Problem: getRefRecord() in async context may return stale data
// The GlideElement snapshot captured at queue time carries cached
// display values that diverge from the committed database state.
// ✕ WRONG — unreliable in async Business Rules
var refRecord = current.assignment_group.getRefRecord();
var manager = refRecord.manager.toString(); // may be stale
// ✓ CORRECT — explicit re-query via sys_id
// toString() on a reference field reliably returns the sys_id
// from the snapshot — only the cached display values are stale.
var groupSysId = current.assignment_group.toString();
if (groupSysId) {
var group = new GlideRecord('sys_user_group');
if (group.get(groupSysId)) {
// Fresh database read — always correct
var manager = group.manager.toString();
var groupName = group.name.toString();
// Additional dot-walk? Use another explicit query.
if (manager) {
var managerRec = new GlideRecord('sys_user');
if (managerRec.get(manager)) {
var email = managerRec.email.toString();
}
}
}
}
current as a bag of sys_ids and primitive values only. Never call getRefRecord(), never dot-walk, never call getDisplayValue() on reference fields. Read the full article →// In your Scripted REST Resource handler
// This pattern enforces field-level access control at the API layer
(function process(request, response) {
// Build the full response object
var record = new GlideRecord('incident');
if (!record.get(request.pathParams.sys_id)) {
response.setStatus(404);
return;
}
var payload = {
sys_id: record.sys_id.toString(),
number: record.number.toString(),
short_desc: record.short_description.toString(),
state: record.state.toString(),
// Sensitive fields — conditionally included
caller_id: record.caller_id.toString(),
work_notes: record.work_notes.toString(),
cost_center: record.u_cost_center.toString()
};
// Determine caller identity from header or OAuth token
var callerSysId = request.getHeader('X-Caller-SysId');
// Strip fields based on caller's roles
if (!callerHasRole(callerSysId, 'u_api_sensitive_data')) {
delete payload.caller_id;
delete payload.work_notes;
delete payload.cost_center;
}
response.setContentType('application/json');
response.setBody(JSON.stringify(payload));
})(request, response);
// Helper — checks if a user sys_id has a given role
function callerHasRole(userSysId, roleName) {
var roleCheck = new GlideRecord('sys_user_has_role');
roleCheck.addQuery('user', userSysId);
roleCheck.addQuery('role.name', roleName);
roleCheck.addQuery('state', 'active');
roleCheck.query();
return roleCheck.hasNext();
}
// Dot-walking works fine in sync Business Rules.
// In async context, each reference hop may resolve stale data.
// Replace dot-walk chains with sequential explicit GlideRecord queries.
// Goal: get the email of the manager of the incident's assignment group
// ✕ WRONG in async — each dot is a potential stale reference
// var email = current.assignment_group.manager.email.toString();
// ✓ CORRECT — explicit chain with null checks at each step
var email = getGroupManagerEmail(current.assignment_group.toString());
function getGroupManagerEmail(groupSysId) {
if (!groupSysId) return null;
var group = new GlideRecord('sys_user_group');
if (!group.get(groupSysId)) return null;
var managerSysId = group.manager.toString();
if (!managerSysId) return null;
var manager = new GlideRecord('sys_user');
if (!manager.get(managerSysId)) return null;
var email = manager.email.toString();
return email || null;
}
// This pattern is:
// - Safe in both sync and async context
// - Explicit and debuggable (each step can be logged)
// - Null-safe at every reference boundary
// - Slightly more verbose — worth it every time
GlideRecord.get() is a fresh database read — immune to snapshot staleness. Include null checks at every step: in async, it's entirely possible for a reference that existed at queue time to have been modified or deleted by execution time.