[{"data":1,"prerenderedAt":1750},["ShallowReactive",2],{"page-\u002Fblog\u002Fservicenow-scripted-rest-api-best-practices":3},{"id":4,"title":5,"author":6,"authorUrl":7,"body":8,"date":1720,"dateUpdated":1720,"description":1721,"dqid":1720,"excerpt":1722,"extension":1723,"faq":1724,"headline":1743,"meta":1744,"navigation":95,"path":1745,"seo":1746,"socialImage":1747,"stem":1748,"tags":1722,"__hash__":1749},"content\u002Fblog\u002Fservicenow-scripted-rest-api-best-practices.md","ServiceNow Scripted REST API: 10 Best Practices for Secure, Production-Ready APIs","SN-Tricks","https:\u002F\u002Fsn-tricks.com\u002Fabout",{"type":9,"value":10,"toc":1707},"minimark",[11,15,18,23,26,352,355,359,362,576,579,583,586,646,649,696,700,703,874,877,881,884,1206,1213,1217,1220,1358,1361,1365,1368,1557,1560,1564,1567,1570,1584,1587,1591,1594,1597,1639,1642,1646,1649,1652,1690,1693,1697,1700,1703],[12,13,14],"p",{},"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.",[12,16,17],{},"These 10 practices will help you build Scripted REST APIs that are secure, maintainable, and production-ready from day one.",[19,20,22],"h2",{"id":21},"_1-validate-every-request-parameter","1. Validate Every Request Parameter",[12,24,25],{},"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.",[27,28,33],"pre",{"className":29,"code":30,"language":31,"meta":32,"style":32},"language-javascript shiki shiki-themes github-light github-light","(function process(\u002F* RESTRequestAPI *\u002F request, \u002F* RESTResponseAPI *\u002F response) {\n    var params = request.queryParams;\n\n    \u002F\u002F Validate required params exist and are non-empty\n    if (!params.limit || isNaN(parseInt(params.limit, 10))) {\n        return badRequest(response, 'Missing or invalid \"limit\" parameter');\n    }\n\n    var limit = Math.min(parseInt(params.limit, 10), 100); \u002F\u002F Cap at 100\n    var offset = parseInt(params.offset || '0', 10);\n\n    \u002F\u002F Validate offset\n    if (offset \u003C 0) {\n        return badRequest(response, 'Offset must be non-negative');\n    }\n\n    \u002F\u002F Process with validated values...\n})(request, response);\n\nfunction badRequest(response, message) {\n    response.setStatus(400);\n    response.setBody({ error: message });\n}\n","javascript","",[34,35,36,75,90,97,103,139,158,164,169,205,232,237,243,259,273,278,283,289,295,300,319,335,346],"code",{"__ignoreMap":32},[37,38,41,45,49,53,55,59,63,66,69,72],"span",{"class":39,"line":40},"line",1,[37,42,44],{"class":43},"sKWpL","(",[37,46,48],{"class":47},"sCydW","function",[37,50,52],{"class":51},"se37E"," process",[37,54,44],{"class":43},[37,56,58],{"class":57},"sJ8bj","\u002F* RESTRequestAPI *\u002F",[37,60,62],{"class":61},"sj_tP"," request",[37,64,65],{"class":43},", ",[37,67,68],{"class":57},"\u002F* RESTResponseAPI *\u002F",[37,70,71],{"class":61}," response",[37,73,74],{"class":43},") {\n",[37,76,78,81,84,87],{"class":39,"line":77},2,[37,79,80],{"class":47},"    var",[37,82,83],{"class":43}," params ",[37,85,86],{"class":47},"=",[37,88,89],{"class":43}," request.queryParams;\n",[37,91,93],{"class":39,"line":92},3,[37,94,96],{"emptyLinePlaceholder":95},true,"\n",[37,98,100],{"class":39,"line":99},4,[37,101,102],{"class":57},"    \u002F\u002F Validate required params exist and are non-empty\n",[37,104,106,109,112,115,118,121,124,126,129,132,136],{"class":39,"line":105},5,[37,107,108],{"class":47},"    if",[37,110,111],{"class":43}," (",[37,113,114],{"class":47},"!",[37,116,117],{"class":43},"params.limit ",[37,119,120],{"class":47},"||",[37,122,123],{"class":51}," isNaN",[37,125,44],{"class":43},[37,127,128],{"class":51},"parseInt",[37,130,131],{"class":43},"(params.limit, ",[37,133,135],{"class":134},"sMN4m","10",[37,137,138],{"class":43},"))) {\n",[37,140,142,145,148,151,155],{"class":39,"line":141},6,[37,143,144],{"class":47},"        return",[37,146,147],{"class":51}," badRequest",[37,149,150],{"class":43},"(response, ",[37,152,154],{"class":153},"sOTlB","'Missing or invalid \"limit\" parameter'",[37,156,157],{"class":43},");\n",[37,159,161],{"class":39,"line":160},7,[37,162,163],{"class":43},"    }\n",[37,165,167],{"class":39,"line":166},8,[37,168,96],{"emptyLinePlaceholder":95},[37,170,172,174,177,179,182,185,187,189,191,193,196,199,202],{"class":39,"line":171},9,[37,173,80],{"class":47},[37,175,176],{"class":43}," limit ",[37,178,86],{"class":47},[37,180,181],{"class":43}," Math.",[37,183,184],{"class":51},"min",[37,186,44],{"class":43},[37,188,128],{"class":51},[37,190,131],{"class":43},[37,192,135],{"class":134},[37,194,195],{"class":43},"), ",[37,197,198],{"class":134},"100",[37,200,201],{"class":43},"); ",[37,203,204],{"class":57},"\u002F\u002F Cap at 100\n",[37,206,208,210,213,215,218,221,223,226,228,230],{"class":39,"line":207},10,[37,209,80],{"class":47},[37,211,212],{"class":43}," offset ",[37,214,86],{"class":47},[37,216,217],{"class":51}," parseInt",[37,219,220],{"class":43},"(params.offset ",[37,222,120],{"class":47},[37,224,225],{"class":153}," '0'",[37,227,65],{"class":43},[37,229,135],{"class":134},[37,231,157],{"class":43},[37,233,235],{"class":39,"line":234},11,[37,236,96],{"emptyLinePlaceholder":95},[37,238,240],{"class":39,"line":239},12,[37,241,242],{"class":57},"    \u002F\u002F Validate offset\n",[37,244,246,248,251,254,257],{"class":39,"line":245},13,[37,247,108],{"class":47},[37,249,250],{"class":43}," (offset ",[37,252,253],{"class":47},"\u003C",[37,255,256],{"class":134}," 0",[37,258,74],{"class":43},[37,260,262,264,266,268,271],{"class":39,"line":261},14,[37,263,144],{"class":47},[37,265,147],{"class":51},[37,267,150],{"class":43},[37,269,270],{"class":153},"'Offset must be non-negative'",[37,272,157],{"class":43},[37,274,276],{"class":39,"line":275},15,[37,277,163],{"class":43},[37,279,281],{"class":39,"line":280},16,[37,282,96],{"emptyLinePlaceholder":95},[37,284,286],{"class":39,"line":285},17,[37,287,288],{"class":57},"    \u002F\u002F Process with validated values...\n",[37,290,292],{"class":39,"line":291},18,[37,293,294],{"class":43},"})(request, response);\n",[37,296,298],{"class":39,"line":297},19,[37,299,96],{"emptyLinePlaceholder":95},[37,301,303,305,307,309,312,314,317],{"class":39,"line":302},20,[37,304,48],{"class":47},[37,306,147],{"class":51},[37,308,44],{"class":43},[37,310,311],{"class":61},"response",[37,313,65],{"class":43},[37,315,316],{"class":61},"message",[37,318,74],{"class":43},[37,320,322,325,328,330,333],{"class":39,"line":321},21,[37,323,324],{"class":43},"    response.",[37,326,327],{"class":51},"setStatus",[37,329,44],{"class":43},[37,331,332],{"class":134},"400",[37,334,157],{"class":43},[37,336,338,340,343],{"class":39,"line":337},22,[37,339,324],{"class":43},[37,341,342],{"class":51},"setBody",[37,344,345],{"class":43},"({ error: message });\n",[37,347,349],{"class":39,"line":348},23,[37,350,351],{"class":43},"}\n",[12,353,354],{},"Apply allowlists for enum values, length limits for strings, and type checks for numbers. Reject early with clear error messages.",[19,356,358],{"id":357},"_2-use-basic-auth-over-https-never-plain-http","2. Use Basic Auth Over HTTPS — Never Plain HTTP",[12,360,361],{},"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.",[27,363,365],{"className":29,"code":364,"language":31,"meta":32,"style":32},"var auth = request.getHeader('Authorization');\nif (!auth || !auth.startsWith('Basic ')) {\n    response.setStatus(401);\n    response.setHeader('WWW-Authenticate', 'Basic realm=\"SN\"');\n    response.setBody({ error: 'Authentication required' });\n    return;\n}\n\nvar decoded = new global.JSON().decode(\n    new global.CompatibilityUtils().base64Decode(auth.substring(6))\n);\nvar creds = decoded.split(':', 2);\n\u002F\u002F Validate against stored credentials\n",[34,366,367,390,421,434,453,468,476,480,484,511,540,544,571],{"__ignoreMap":32},[37,368,369,372,375,377,380,383,385,388],{"class":39,"line":40},[37,370,371],{"class":47},"var",[37,373,374],{"class":43}," auth ",[37,376,86],{"class":47},[37,378,379],{"class":43}," request.",[37,381,382],{"class":51},"getHeader",[37,384,44],{"class":43},[37,386,387],{"class":153},"'Authorization'",[37,389,157],{"class":43},[37,391,392,395,397,399,402,404,407,410,413,415,418],{"class":39,"line":77},[37,393,394],{"class":47},"if",[37,396,111],{"class":43},[37,398,114],{"class":47},[37,400,401],{"class":43},"auth ",[37,403,120],{"class":47},[37,405,406],{"class":47}," !",[37,408,409],{"class":43},"auth.",[37,411,412],{"class":51},"startsWith",[37,414,44],{"class":43},[37,416,417],{"class":153},"'Basic '",[37,419,420],{"class":43},")) {\n",[37,422,423,425,427,429,432],{"class":39,"line":92},[37,424,324],{"class":43},[37,426,327],{"class":51},[37,428,44],{"class":43},[37,430,431],{"class":134},"401",[37,433,157],{"class":43},[37,435,436,438,441,443,446,448,451],{"class":39,"line":99},[37,437,324],{"class":43},[37,439,440],{"class":51},"setHeader",[37,442,44],{"class":43},[37,444,445],{"class":153},"'WWW-Authenticate'",[37,447,65],{"class":43},[37,449,450],{"class":153},"'Basic realm=\"SN\"'",[37,452,157],{"class":43},[37,454,455,457,459,462,465],{"class":39,"line":105},[37,456,324],{"class":43},[37,458,342],{"class":51},[37,460,461],{"class":43},"({ error: ",[37,463,464],{"class":153},"'Authentication required'",[37,466,467],{"class":43}," });\n",[37,469,470,473],{"class":39,"line":141},[37,471,472],{"class":47},"    return",[37,474,475],{"class":43},";\n",[37,477,478],{"class":39,"line":160},[37,479,351],{"class":43},[37,481,482],{"class":39,"line":166},[37,483,96],{"emptyLinePlaceholder":95},[37,485,486,488,491,493,496,499,502,505,508],{"class":39,"line":171},[37,487,371],{"class":47},[37,489,490],{"class":43}," decoded ",[37,492,86],{"class":47},[37,494,495],{"class":47}," new",[37,497,498],{"class":43}," global.",[37,500,501],{"class":51},"JSON",[37,503,504],{"class":43},"().",[37,506,507],{"class":51},"decode",[37,509,510],{"class":43},"(\n",[37,512,513,516,518,521,523,526,529,532,534,537],{"class":39,"line":207},[37,514,515],{"class":47},"    new",[37,517,498],{"class":43},[37,519,520],{"class":51},"CompatibilityUtils",[37,522,504],{"class":43},[37,524,525],{"class":51},"base64Decode",[37,527,528],{"class":43},"(auth.",[37,530,531],{"class":51},"substring",[37,533,44],{"class":43},[37,535,536],{"class":134},"6",[37,538,539],{"class":43},"))\n",[37,541,542],{"class":39,"line":234},[37,543,157],{"class":43},[37,545,546,548,551,553,556,559,561,564,566,569],{"class":39,"line":239},[37,547,371],{"class":47},[37,549,550],{"class":43}," creds ",[37,552,86],{"class":47},[37,554,555],{"class":43}," decoded.",[37,557,558],{"class":51},"split",[37,560,44],{"class":43},[37,562,563],{"class":153},"':'",[37,565,65],{"class":43},[37,567,568],{"class":134},"2",[37,570,157],{"class":43},[37,572,573],{"class":39,"line":245},[37,574,575],{"class":57},"\u002F\u002F Validate against stored credentials\n",[12,577,578],{},"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.",[19,580,582],{"id":581},"_3-set-explicit-response-types-and-status-codes","3. Set Explicit Response Types and Status Codes",[12,584,585],{},"Ambiguous responses cause consumer confusion and broken integrations. Always set the HTTP status code explicitly and return a consistent response envelope.",[27,587,589],{"className":29,"code":588,"language":31,"meta":32,"style":32},"var result = processIncidentRequest(request);\nresponse.setStatus(result.statusCode || 200);\nresponse.setContentType('application\u002Fjson');\nresponse.setBody(result.body);\n",[34,590,591,606,623,637],{"__ignoreMap":32},[37,592,593,595,598,600,603],{"class":39,"line":40},[37,594,371],{"class":47},[37,596,597],{"class":43}," result ",[37,599,86],{"class":47},[37,601,602],{"class":51}," processIncidentRequest",[37,604,605],{"class":43},"(request);\n",[37,607,608,611,613,616,618,621],{"class":39,"line":77},[37,609,610],{"class":43},"response.",[37,612,327],{"class":51},[37,614,615],{"class":43},"(result.statusCode ",[37,617,120],{"class":47},[37,619,620],{"class":134}," 200",[37,622,157],{"class":43},[37,624,625,627,630,632,635],{"class":39,"line":92},[37,626,610],{"class":43},[37,628,629],{"class":51},"setContentType",[37,631,44],{"class":43},[37,633,634],{"class":153},"'application\u002Fjson'",[37,636,157],{"class":43},[37,638,639,641,643],{"class":39,"line":99},[37,640,610],{"class":43},[37,642,342],{"class":51},[37,644,645],{"class":43},"(result.body);\n",[12,647,648],{},"Use standard status codes deliberately:",[650,651,652,660,666,672,678,684,690],"ul",{},[653,654,655,659],"li",{},[656,657,658],"strong",{},"200 OK"," — success with data",[653,661,662,665],{},[656,663,664],{},"201 Created"," — resource created (include the new resource URI in Location header)",[653,667,668,671],{},[656,669,670],{},"400 Bad Request"," — invalid input",[653,673,674,677],{},[656,675,676],{},"401 Unauthorized"," — missing or invalid auth",[653,679,680,683],{},[656,681,682],{},"404 Not Found"," — resource doesn't exist",[653,685,686,689],{},[656,687,688],{},"429 Too Many Requests"," — rate limit exceeded",[653,691,692,695],{},[656,693,694],{},"500 Internal Server Error"," — unexpected server failure (never expose stack traces)",[19,697,699],{"id":698},"_4-return-consistent-json-envelopes","4. Return Consistent JSON Envelopes",[12,701,702],{},"Define a response envelope and use it everywhere. Consumers should always get the same structure:",[27,704,706],{"className":29,"code":705,"language":31,"meta":32,"style":32},"function okResponse(data, total, limit, offset) {\n    return {\n        result: data,\n        meta: {\n            total: total,\n            limit: limit,\n            offset: offset,\n            hasMore: (offset + data.length) \u003C total\n        }\n    };\n}\n\nfunction errorResponse(code, message, details) {\n    return {\n        error: {\n            code: code,\n            message: message,\n            details: details || null\n        }\n    };\n}\n",[34,707,708,737,744,749,754,759,764,769,791,796,801,805,809,831,837,842,847,852,862,866,870],{"__ignoreMap":32},[37,709,710,712,715,717,720,722,725,727,730,732,735],{"class":39,"line":40},[37,711,48],{"class":47},[37,713,714],{"class":51}," okResponse",[37,716,44],{"class":43},[37,718,719],{"class":61},"data",[37,721,65],{"class":43},[37,723,724],{"class":61},"total",[37,726,65],{"class":43},[37,728,729],{"class":61},"limit",[37,731,65],{"class":43},[37,733,734],{"class":61},"offset",[37,736,74],{"class":43},[37,738,739,741],{"class":39,"line":77},[37,740,472],{"class":47},[37,742,743],{"class":43}," {\n",[37,745,746],{"class":39,"line":92},[37,747,748],{"class":43},"        result: data,\n",[37,750,751],{"class":39,"line":99},[37,752,753],{"class":43},"        meta: {\n",[37,755,756],{"class":39,"line":105},[37,757,758],{"class":43},"            total: total,\n",[37,760,761],{"class":39,"line":141},[37,762,763],{"class":43},"            limit: limit,\n",[37,765,766],{"class":39,"line":160},[37,767,768],{"class":43},"            offset: offset,\n",[37,770,771,774,777,780,783,786,788],{"class":39,"line":166},[37,772,773],{"class":43},"            hasMore: (offset ",[37,775,776],{"class":47},"+",[37,778,779],{"class":43}," data.",[37,781,782],{"class":134},"length",[37,784,785],{"class":43},") ",[37,787,253],{"class":47},[37,789,790],{"class":43}," total\n",[37,792,793],{"class":39,"line":171},[37,794,795],{"class":43},"        }\n",[37,797,798],{"class":39,"line":207},[37,799,800],{"class":43},"    };\n",[37,802,803],{"class":39,"line":234},[37,804,351],{"class":43},[37,806,807],{"class":39,"line":239},[37,808,96],{"emptyLinePlaceholder":95},[37,810,811,813,816,818,820,822,824,826,829],{"class":39,"line":245},[37,812,48],{"class":47},[37,814,815],{"class":51}," errorResponse",[37,817,44],{"class":43},[37,819,34],{"class":61},[37,821,65],{"class":43},[37,823,316],{"class":61},[37,825,65],{"class":43},[37,827,828],{"class":61},"details",[37,830,74],{"class":43},[37,832,833,835],{"class":39,"line":261},[37,834,472],{"class":47},[37,836,743],{"class":43},[37,838,839],{"class":39,"line":275},[37,840,841],{"class":43},"        error: {\n",[37,843,844],{"class":39,"line":280},[37,845,846],{"class":43},"            code: code,\n",[37,848,849],{"class":39,"line":285},[37,850,851],{"class":43},"            message: message,\n",[37,853,854,857,859],{"class":39,"line":291},[37,855,856],{"class":43},"            details: details ",[37,858,120],{"class":47},[37,860,861],{"class":134}," null\n",[37,863,864],{"class":39,"line":297},[37,865,795],{"class":43},[37,867,868],{"class":39,"line":302},[37,869,800],{"class":43},[37,871,872],{"class":39,"line":321},[37,873,351],{"class":43},[12,875,876],{},"Consistent envelopes make it trivial for consumers to parse success vs. error, handle pagination, and debug issues in production.",[19,878,880],{"id":879},"_5-implement-pagination-from-the-start","5. Implement Pagination from the Start",[12,882,883],{},"If your API returns lists, implement pagination from the beginning. Retrofit it later and you'll break existing consumers.",[27,885,887],{"className":29,"code":886,"language":31,"meta":32,"style":32},"(function process(request, response) {\n    var limit = Math.min(parseInt(request.queryParams.limit || '20', 10), 100);\n    var offset = parseInt(request.queryParams.offset || '0', 10);\n\n    var gr = new GlideRecord('incident');\n    gr.addQuery('active', true);\n    gr.orderByDesc('sys_created_on');\n    gr.setRowLimit(limit);\n    gr.setWorkflow(false);\n\n    \u002F\u002F For very large tables, use GlideAggregate for count instead of getRowCount()\n    \u002F\u002F to avoid the overhead of a full SELECT COUNT(*) on every request\n    var total = gr.getRowCount();\n    gr.absoluteWindow(offset, limit);\n\n    var results = [];\n    while (gr.next()) {\n        results.push({\n            sys_id: gr.sys_id.toString(),\n            number: gr.number.toString(),\n            short_description: gr.short_description.toString(),\n            state: gr.state.getDisplayValue()\n        });\n    }\n\n    response.setBody(okResponse(results, total, limit, offset));\n})(request, response);\n",[34,888,889,908,942,965,969,990,1010,1024,1034,1048,1052,1057,1062,1080,1090,1094,1106,1120,1131,1142,1151,1160,1171,1176,1181,1186,1201],{"__ignoreMap":32},[37,890,891,893,895,897,899,902,904,906],{"class":39,"line":40},[37,892,44],{"class":43},[37,894,48],{"class":47},[37,896,52],{"class":51},[37,898,44],{"class":43},[37,900,901],{"class":61},"request",[37,903,65],{"class":43},[37,905,311],{"class":61},[37,907,74],{"class":43},[37,909,910,912,914,916,918,920,922,924,927,929,932,934,936,938,940],{"class":39,"line":77},[37,911,80],{"class":47},[37,913,176],{"class":43},[37,915,86],{"class":47},[37,917,181],{"class":43},[37,919,184],{"class":51},[37,921,44],{"class":43},[37,923,128],{"class":51},[37,925,926],{"class":43},"(request.queryParams.limit ",[37,928,120],{"class":47},[37,930,931],{"class":153}," '20'",[37,933,65],{"class":43},[37,935,135],{"class":134},[37,937,195],{"class":43},[37,939,198],{"class":134},[37,941,157],{"class":43},[37,943,944,946,948,950,952,955,957,959,961,963],{"class":39,"line":92},[37,945,80],{"class":47},[37,947,212],{"class":43},[37,949,86],{"class":47},[37,951,217],{"class":51},[37,953,954],{"class":43},"(request.queryParams.offset ",[37,956,120],{"class":47},[37,958,225],{"class":153},[37,960,65],{"class":43},[37,962,135],{"class":134},[37,964,157],{"class":43},[37,966,967],{"class":39,"line":99},[37,968,96],{"emptyLinePlaceholder":95},[37,970,971,973,976,978,980,983,985,988],{"class":39,"line":105},[37,972,80],{"class":47},[37,974,975],{"class":43}," gr ",[37,977,86],{"class":47},[37,979,495],{"class":47},[37,981,982],{"class":51}," GlideRecord",[37,984,44],{"class":43},[37,986,987],{"class":153},"'incident'",[37,989,157],{"class":43},[37,991,992,995,998,1000,1003,1005,1008],{"class":39,"line":141},[37,993,994],{"class":43},"    gr.",[37,996,997],{"class":51},"addQuery",[37,999,44],{"class":43},[37,1001,1002],{"class":153},"'active'",[37,1004,65],{"class":43},[37,1006,1007],{"class":134},"true",[37,1009,157],{"class":43},[37,1011,1012,1014,1017,1019,1022],{"class":39,"line":160},[37,1013,994],{"class":43},[37,1015,1016],{"class":51},"orderByDesc",[37,1018,44],{"class":43},[37,1020,1021],{"class":153},"'sys_created_on'",[37,1023,157],{"class":43},[37,1025,1026,1028,1031],{"class":39,"line":166},[37,1027,994],{"class":43},[37,1029,1030],{"class":51},"setRowLimit",[37,1032,1033],{"class":43},"(limit);\n",[37,1035,1036,1038,1041,1043,1046],{"class":39,"line":171},[37,1037,994],{"class":43},[37,1039,1040],{"class":51},"setWorkflow",[37,1042,44],{"class":43},[37,1044,1045],{"class":134},"false",[37,1047,157],{"class":43},[37,1049,1050],{"class":39,"line":207},[37,1051,96],{"emptyLinePlaceholder":95},[37,1053,1054],{"class":39,"line":234},[37,1055,1056],{"class":57},"    \u002F\u002F For very large tables, use GlideAggregate for count instead of getRowCount()\n",[37,1058,1059],{"class":39,"line":239},[37,1060,1061],{"class":57},"    \u002F\u002F to avoid the overhead of a full SELECT COUNT(*) on every request\n",[37,1063,1064,1066,1069,1071,1074,1077],{"class":39,"line":245},[37,1065,80],{"class":47},[37,1067,1068],{"class":43}," total ",[37,1070,86],{"class":47},[37,1072,1073],{"class":43}," gr.",[37,1075,1076],{"class":51},"getRowCount",[37,1078,1079],{"class":43},"();\n",[37,1081,1082,1084,1087],{"class":39,"line":261},[37,1083,994],{"class":43},[37,1085,1086],{"class":51},"absoluteWindow",[37,1088,1089],{"class":43},"(offset, limit);\n",[37,1091,1092],{"class":39,"line":275},[37,1093,96],{"emptyLinePlaceholder":95},[37,1095,1096,1098,1101,1103],{"class":39,"line":280},[37,1097,80],{"class":47},[37,1099,1100],{"class":43}," results ",[37,1102,86],{"class":47},[37,1104,1105],{"class":43}," [];\n",[37,1107,1108,1111,1114,1117],{"class":39,"line":285},[37,1109,1110],{"class":47},"    while",[37,1112,1113],{"class":43}," (gr.",[37,1115,1116],{"class":51},"next",[37,1118,1119],{"class":43},"()) {\n",[37,1121,1122,1125,1128],{"class":39,"line":291},[37,1123,1124],{"class":43},"        results.",[37,1126,1127],{"class":51},"push",[37,1129,1130],{"class":43},"({\n",[37,1132,1133,1136,1139],{"class":39,"line":297},[37,1134,1135],{"class":43},"            sys_id: gr.sys_id.",[37,1137,1138],{"class":51},"toString",[37,1140,1141],{"class":43},"(),\n",[37,1143,1144,1147,1149],{"class":39,"line":302},[37,1145,1146],{"class":43},"            number: gr.number.",[37,1148,1138],{"class":51},[37,1150,1141],{"class":43},[37,1152,1153,1156,1158],{"class":39,"line":321},[37,1154,1155],{"class":43},"            short_description: gr.short_description.",[37,1157,1138],{"class":51},[37,1159,1141],{"class":43},[37,1161,1162,1165,1168],{"class":39,"line":337},[37,1163,1164],{"class":43},"            state: gr.state.",[37,1166,1167],{"class":51},"getDisplayValue",[37,1169,1170],{"class":43},"()\n",[37,1172,1173],{"class":39,"line":348},[37,1174,1175],{"class":43},"        });\n",[37,1177,1179],{"class":39,"line":1178},24,[37,1180,163],{"class":43},[37,1182,1184],{"class":39,"line":1183},25,[37,1185,96],{"emptyLinePlaceholder":95},[37,1187,1189,1191,1193,1195,1198],{"class":39,"line":1188},26,[37,1190,324],{"class":43},[37,1192,342],{"class":51},[37,1194,44],{"class":43},[37,1196,1197],{"class":51},"okResponse",[37,1199,1200],{"class":43},"(results, total, limit, offset));\n",[37,1202,1204],{"class":39,"line":1203},27,[37,1205,294],{"class":43},[12,1207,1208,1209,1212],{},"Use ",[34,1210,1211],{},"getRowCount()"," cautiously — on very large tables prefer GlideAggregate with a separate count query to avoid the overhead of a full count on every request.",[19,1214,1216],{"id":1215},"_6-handle-errors-gracefully-never-leak-stack-traces","6. Handle Errors Gracefully — Never Leak Stack Traces",[12,1218,1219],{},"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.",[27,1221,1223],{"className":29,"code":1222,"language":31,"meta":32,"style":32},"try {\n    var result = riskyOperation();\n    response.setBody(okResponse(result));\n    response.setStatus(200);\n} catch (e) {\n    gs.error('Scripted REST API error: ' + e.message + '\\n' + e.stack);\n    response.setStatus(500);\n    response.setBody(errorResponse('INTERNAL_ERROR', 'An unexpected error occurred'));\n}\n",[34,1224,1225,1232,1245,1258,1271,1282,1317,1330,1354],{"__ignoreMap":32},[37,1226,1227,1230],{"class":39,"line":40},[37,1228,1229],{"class":47},"try",[37,1231,743],{"class":43},[37,1233,1234,1236,1238,1240,1243],{"class":39,"line":77},[37,1235,80],{"class":47},[37,1237,597],{"class":43},[37,1239,86],{"class":47},[37,1241,1242],{"class":51}," riskyOperation",[37,1244,1079],{"class":43},[37,1246,1247,1249,1251,1253,1255],{"class":39,"line":92},[37,1248,324],{"class":43},[37,1250,342],{"class":51},[37,1252,44],{"class":43},[37,1254,1197],{"class":51},[37,1256,1257],{"class":43},"(result));\n",[37,1259,1260,1262,1264,1266,1269],{"class":39,"line":99},[37,1261,324],{"class":43},[37,1263,327],{"class":51},[37,1265,44],{"class":43},[37,1267,1268],{"class":134},"200",[37,1270,157],{"class":43},[37,1272,1273,1276,1279],{"class":39,"line":105},[37,1274,1275],{"class":43},"} ",[37,1277,1278],{"class":47},"catch",[37,1280,1281],{"class":43}," (e) {\n",[37,1283,1284,1287,1290,1292,1295,1298,1301,1303,1306,1309,1312,1314],{"class":39,"line":141},[37,1285,1286],{"class":43},"    gs.",[37,1288,1289],{"class":51},"error",[37,1291,44],{"class":43},[37,1293,1294],{"class":153},"'Scripted REST API error: '",[37,1296,1297],{"class":47}," +",[37,1299,1300],{"class":43}," e.message ",[37,1302,776],{"class":47},[37,1304,1305],{"class":153}," '",[37,1307,1308],{"class":134},"\\n",[37,1310,1311],{"class":153},"'",[37,1313,1297],{"class":47},[37,1315,1316],{"class":43}," e.stack);\n",[37,1318,1319,1321,1323,1325,1328],{"class":39,"line":160},[37,1320,324],{"class":43},[37,1322,327],{"class":51},[37,1324,44],{"class":43},[37,1326,1327],{"class":134},"500",[37,1329,157],{"class":43},[37,1331,1332,1334,1336,1338,1341,1343,1346,1348,1351],{"class":39,"line":166},[37,1333,324],{"class":43},[37,1335,342],{"class":51},[37,1337,44],{"class":43},[37,1339,1340],{"class":51},"errorResponse",[37,1342,44],{"class":43},[37,1344,1345],{"class":153},"'INTERNAL_ERROR'",[37,1347,65],{"class":43},[37,1349,1350],{"class":153},"'An unexpected error occurred'",[37,1352,1353],{"class":43},"));\n",[37,1355,1356],{"class":39,"line":171},[37,1357,351],{"class":43},[12,1359,1360],{},"Consider differentiating expected errors (business rule rejections) from unexpected errors (exceptions) — both return 500 to the consumer, but logging levels differ.",[19,1362,1364],{"id":1363},"_7-log-request-metadata-for-audit-and-debugging","7. Log Request Metadata for Audit and Debugging",[12,1366,1367],{},"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.",[27,1369,1371],{"className":29,"code":1370,"language":31,"meta":32,"style":32},"(function process(request, response) {\n    var startMs = GlideDateTime.getNumericSessionDateTime();\n\n    \u002F\u002F Log request\n    gs.info('REST API: ' + request.method + ' ' + request.path +\n        ' | user=' + request.getHeader('X-User-ID') +\n        ' | ip=' + request.getHeader('X-Forwarded-For'));\n\n    \u002F\u002F ... process request ...\n\n    var duration = GlideDateTime.getNumericSessionDateTime() - startMs;\n    gs.info('REST API response: ' + response.getStatusCode() +\n        ' | duration_ms=' + duration);\n})(request, response);\n",[34,1372,1373,1391,1408,1412,1417,1447,1467,1485,1489,1494,1498,1520,1543,1553],{"__ignoreMap":32},[37,1374,1375,1377,1379,1381,1383,1385,1387,1389],{"class":39,"line":40},[37,1376,44],{"class":43},[37,1378,48],{"class":47},[37,1380,52],{"class":51},[37,1382,44],{"class":43},[37,1384,901],{"class":61},[37,1386,65],{"class":43},[37,1388,311],{"class":61},[37,1390,74],{"class":43},[37,1392,1393,1395,1398,1400,1403,1406],{"class":39,"line":77},[37,1394,80],{"class":47},[37,1396,1397],{"class":43}," startMs ",[37,1399,86],{"class":47},[37,1401,1402],{"class":43}," GlideDateTime.",[37,1404,1405],{"class":51},"getNumericSessionDateTime",[37,1407,1079],{"class":43},[37,1409,1410],{"class":39,"line":92},[37,1411,96],{"emptyLinePlaceholder":95},[37,1413,1414],{"class":39,"line":99},[37,1415,1416],{"class":57},"    \u002F\u002F Log request\n",[37,1418,1419,1421,1424,1426,1429,1431,1434,1436,1439,1441,1444],{"class":39,"line":105},[37,1420,1286],{"class":43},[37,1422,1423],{"class":51},"info",[37,1425,44],{"class":43},[37,1427,1428],{"class":153},"'REST API: '",[37,1430,1297],{"class":47},[37,1432,1433],{"class":43}," request.method ",[37,1435,776],{"class":47},[37,1437,1438],{"class":153}," ' '",[37,1440,1297],{"class":47},[37,1442,1443],{"class":43}," request.path ",[37,1445,1446],{"class":47},"+\n",[37,1448,1449,1452,1454,1456,1458,1460,1463,1465],{"class":39,"line":141},[37,1450,1451],{"class":153},"        ' | user='",[37,1453,1297],{"class":47},[37,1455,379],{"class":43},[37,1457,382],{"class":51},[37,1459,44],{"class":43},[37,1461,1462],{"class":153},"'X-User-ID'",[37,1464,785],{"class":43},[37,1466,1446],{"class":47},[37,1468,1469,1472,1474,1476,1478,1480,1483],{"class":39,"line":160},[37,1470,1471],{"class":153},"        ' | ip='",[37,1473,1297],{"class":47},[37,1475,379],{"class":43},[37,1477,382],{"class":51},[37,1479,44],{"class":43},[37,1481,1482],{"class":153},"'X-Forwarded-For'",[37,1484,1353],{"class":43},[37,1486,1487],{"class":39,"line":166},[37,1488,96],{"emptyLinePlaceholder":95},[37,1490,1491],{"class":39,"line":171},[37,1492,1493],{"class":57},"    \u002F\u002F ... process request ...\n",[37,1495,1496],{"class":39,"line":207},[37,1497,96],{"emptyLinePlaceholder":95},[37,1499,1500,1502,1505,1507,1509,1511,1514,1517],{"class":39,"line":234},[37,1501,80],{"class":47},[37,1503,1504],{"class":43}," duration ",[37,1506,86],{"class":47},[37,1508,1402],{"class":43},[37,1510,1405],{"class":51},[37,1512,1513],{"class":43},"() ",[37,1515,1516],{"class":47},"-",[37,1518,1519],{"class":43}," startMs;\n",[37,1521,1522,1524,1526,1528,1531,1533,1536,1539,1541],{"class":39,"line":239},[37,1523,1286],{"class":43},[37,1525,1423],{"class":51},[37,1527,44],{"class":43},[37,1529,1530],{"class":153},"'REST API response: '",[37,1532,1297],{"class":47},[37,1534,1535],{"class":43}," response.",[37,1537,1538],{"class":51},"getStatusCode",[37,1540,1513],{"class":43},[37,1542,1446],{"class":47},[37,1544,1545,1548,1550],{"class":39,"line":245},[37,1546,1547],{"class":153},"        ' | duration_ms='",[37,1549,1297],{"class":47},[37,1551,1552],{"class":43}," duration);\n",[37,1554,1555],{"class":39,"line":261},[37,1556,294],{"class":43},[12,1558,1559],{},"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.",[19,1561,1563],{"id":1562},"_8-use-integrationhub-where-possible-before-going-custom","8. Use IntegrationHub Where Possible Before Going Custom",[12,1565,1566],{},"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.",[12,1568,1569],{},"Custom Scripted REST APIs shine for:",[650,1571,1572,1575,1578,1581],{},[653,1573,1574],{},"Aggregating data from multiple ServiceNow tables",[653,1576,1577],{},"Implementing custom business logic not available in standard actions",[653,1579,1580],{},"Exposing domain-specific endpoints for a specific consumer",[653,1582,1583],{},"Transforming data between ServiceNow and external formats",[12,1585,1586],{},"Use the right tool for the job. A custom Scripted REST API is not the answer to every integration need.",[19,1588,1590],{"id":1589},"_9-document-your-api-with-openapi","9. Document Your API with OpenAPI",[12,1592,1593],{},"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.",[12,1595,1596],{},"At minimum, document:",[650,1598,1599,1609,1615,1621,1627,1633],{},[653,1600,1601,1604,1605,1608],{},[656,1602,1603],{},"Endpoint and method"," (e.g., ",[34,1606,1607],{},"POST \u002Fapi\u002Fsn-custom\u002Fincident\u002Fescalate",")",[653,1610,1611,1614],{},[656,1612,1613],{},"Authentication"," — what headers are required",[653,1616,1617,1620],{},[656,1618,1619],{},"Request body"," — field names, types, required vs. optional, example",[653,1622,1623,1626],{},[656,1624,1625],{},"Response"," — status codes, body structure, example",[653,1628,1629,1632],{},[656,1630,1631],{},"Error codes"," — what each error code means and how to resolve it",[653,1634,1635,1638],{},[656,1636,1637],{},"Rate limits"," — requests per minute\u002Fhour if applicable",[12,1640,1641],{},"Undocumented APIs create confusion, generate support tickets, and get misused.",[19,1643,1645],{"id":1644},"_10-test-with-realistic-data-and-failure-scenarios","10. Test with Realistic Data and Failure Scenarios",[12,1647,1648],{},"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.",[12,1650,1651],{},"Test these scenarios explicitly:",[650,1653,1654,1660,1666,1672,1678,1684],{},[653,1655,1656,1659],{},[656,1657,1658],{},"Missing required parameters"," — confirm 400 response",[653,1661,1662,1665],{},[656,1663,1664],{},"Malformed JSON body"," — confirm 400, not 500",[653,1667,1668,1671],{},[656,1669,1670],{},"Invalid authentication"," — confirm 401, not 200 with empty body",[653,1673,1674,1677],{},[656,1675,1676],{},"Very large result sets"," — confirm pagination works and no memory issues",[653,1679,1680,1683],{},[656,1681,1682],{},"Concurrent requests"," — confirm no race conditions on shared state",[653,1685,1686,1689],{},[656,1687,1688],{},"Timeout behavior"," — confirm external calls have timeouts and the API fails gracefully",[12,1691,1692],{},"Use Postman or curl locally, then validate against your production-like test environment before deploying.",[19,1694,1696],{"id":1695},"final-thoughts","Final Thoughts",[12,1698,1699],{},"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.",[12,1701,1702],{},"Start with validation and consistent envelopes, add proper authentication, and build from there. Your consumers — and your on-call team — will thank you.",[1704,1705,1706],"style",{},"html pre.shiki code .sKWpL, html code.shiki .sKWpL{--shiki-default:#24292E;--shiki-dark:#24292E}html pre.shiki code .sCydW, html code.shiki .sCydW{--shiki-default:#D73A49;--shiki-dark:#D73A49}html pre.shiki code .se37E, html code.shiki .se37E{--shiki-default:#6F42C1;--shiki-dark:#6F42C1}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj_tP, html code.shiki .sj_tP{--shiki-default:#E36209;--shiki-dark:#E36209}html pre.shiki code .sMN4m, html code.shiki .sMN4m{--shiki-default:#005CC5;--shiki-dark:#005CC5}html pre.shiki code .sOTlB, html code.shiki .sOTlB{--shiki-default:#032F62;--shiki-dark:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":32,"searchDepth":77,"depth":77,"links":1708},[1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719],{"id":21,"depth":77,"text":22},{"id":357,"depth":77,"text":358},{"id":581,"depth":77,"text":582},{"id":698,"depth":77,"text":699},{"id":879,"depth":77,"text":880},{"id":1215,"depth":77,"text":1216},{"id":1363,"depth":77,"text":1364},{"id":1562,"depth":77,"text":1563},{"id":1589,"depth":77,"text":1590},{"id":1644,"depth":77,"text":1645},{"id":1695,"depth":77,"text":1696},"2026-09-21","Build secure, maintainable Scripted REST APIs in ServiceNow with proper authentication, input validation, error handling, pagination, and integration patterns used in production environments.",null,"md",[1725,1728,1731,1734,1737,1740],{"question":1726,"answer":1727},"What is a Scripted REST API in ServiceNow?","A Scripted REST API is a custom REST API defined in ServiceNow where you write JavaScript (server-side) to handle HTTP requests and responses. Unlike the table REST API, a scripted API gives you full control over request parsing, business logic, and response formatting.",{"question":1729,"answer":1730},"How do I secure a Scripted REST API in ServiceNow?","Use Basic Auth over HTTPS, validate all incoming request parameters, implement rate limiting via Integration Hub or a GlideRecord counter, and require an API key or OAuth token for external-facing endpoints. Never expose admin credentials in client-facing integrations.",{"question":1732,"answer":1733},"What is the difference between a Scripted REST API and the OOTB Table REST API?","The Table REST API exposes ServiceNow tables automatically with minimal configuration — ideal for CRUD operations. A Scripted REST API lets you define custom endpoints, implement specific business logic, aggregate data from multiple tables, and control exactly what gets returned and under what conditions.",{"question":1735,"answer":1736},"How do I handle pagination in a ServiceNow Scripted REST API?","Use limit and offset query parameters in your request handler. Return a wrapper object containing the data array plus metadata such as total count, current page, and hasMore. This lets consuming applications page through results without loading the entire result set into memory.",{"question":1738,"answer":1739},"Can Scripted REST APIs be called from Flow Designer?","Yes. Flow Designer has a 'Call REST Action' step that can invoke any registered Scripted REST API. For more complex orchestration, use a Scripted REST API as a webhook target from an integration, or call it from a Scripted Action via the RESTMessageV2 API.",{"question":1741,"answer":1742},"How do I log requests and responses for a Scripted REST API?","Use the 'Log' field on the REST Message record to set a log level, or write custom log entries to syslog via gs.info() with a structured format including the request method, path, user, and response status. For production monitoring, consider writing to a custom audit table for later analysis.","10 Scripted REST API Best Practices Every ServiceNow Developer Should Know",{},"\u002Fblog\u002Fservicenow-scripted-rest-api-best-practices",{"title":5,"description":1721},"\u002Fimages\u002Fblog\u002Fservicenow-scripted-rest-api.jpg","blog\u002Fservicenow-scripted-rest-api-best-practices","TQOuen3djRDLqAXzhHtUc5vGnlW768RKbfDZUf9RQzo",1789945516887]