What This Covers
How to wire a real-time external API call into a ServiceNow catalog item so a field populates automatically as the user types — without a page reload. This is the exact object stack, in order, with the gotchas that aren't obvious from the docs.
The use case: account number field → live banking API → auto-fill customer name and account type. But the pattern applies to any external data source you want to pull into a catalog form.
---
The Object Stack
catalog item variable (account_number)
→ onChange client script
→ GlideAjax → client-callable Script Include (bridge)
→ server-side Script Include (business logic)
→ sn_ws.RESTMessageV2 (REST Message + Function)
→ external API
Four ServiceNow objects. Two of them are Script Includes with different roles. Keep them separate — mixing client-callable bridge logic with actual API logic creates a mess.
---
Step 1: REST Message
In System Web Services → Outbound → REST Messages, create a new record:
- Endpoint: your API's base URL
- Authentication: OAuth 2.0 — create an `oauth_entity` and an `oauth_entity_profile`, link the profile here
- Function: create a child `REST Message Function` for the specific endpoint
On the function, set HTTP Method: POST and define the request body with substitution variables:
{"AcctSel": {"AcctKeys": {"AcctId": "${AcctId}", "AcctType": "${AcctType}"}}}
The gotcha with custom headers: Some APIs require vendor-specific headers on every request. Add these on the function, not the parent REST Message. Headers on the parent don't always propagate to functions in every ServiceNow version.
For an API requiring a structured JSON header:
Header name: EFXHeader
Header value: {"OrganizationId":"YOUR_ORG_ID","TrnId":"001"}
---
Step 2: Server-Side Script Include
This is the business logic layer. Not client-callable — it runs on the server only.
The key pattern here is multi-type iteration: if the API requires an account type in the request but your users won't know it, try each type in order and return the first hit.
var AccountLookupService = Class.create();
AccountLookupService.prototype = {
queryAccount: function(accountNumber) {
var types = ['DDA', 'SAV', 'LN', 'SDA', 'CDA', 'LOAN'];
for (var i = 0; i < types.length; i++) {
var result = this._callApi(accountNumber, types[i]);
if (result && result.found) return result;
}
return { found: false };
},
_callApi: function(acctId, acctType) {
try {
var rm = new sn_ws.RESTMessageV2('Your REST Message Name', 'YourFunctionName');
rm.setStringParameter('AcctId', acctId);
rm.setStringParameter('AcctType', acctType);
// Dynamic header if the API needs a unique transaction ID per call
rm.setRequestHeader('EFXHeader',
JSON.stringify({ OrganizationId: 'YOUR_ORG_ID', TrnId: gs.generateGUID() })
);
var response = rm.execute();
var statusCode = response.getStatusCode();
if (statusCode !== 200) return null;
var body = JSON.parse(response.getBody());
// Parse name — real APIs often have deeply nested or inconsistently keyed responses
var name = '';
try {
name = body.AcctInqRs.DepAcctInfo.PostAddr[0].FullName1;
} catch(e) {
// fallback: grep for any FullName key in the raw response
var match = response.getBody().match(/"FullName1":"([^"]+)"/);
name = match ? match[1] : '';
}
if (name) return { found: true, name: name, account_type: acctType };
} catch(e) {
gs.error('AccountLookupService: ' + e.message);
}
return null;
},
type: 'AccountLookupService'
};
Why the nested try/catch on the name parse: Banking API responses are often deeply nested with inconsistent field naming across account types. A defensive fallback regex against the raw body body catches edge cases when the object path breaks.
---
Step 3: Client-Callable Script Include (The Bridge)
This is the GlideAjax bridge. Its only job is to receive the client call, invoke the service, and return a JSON string. Keep it thin.
var AccountLookupAjax = Class.create();
AccountLookupAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
lookupAccount: function() {
var accountNumber = this.getParameter('sysparm_account_number');
if (!accountNumber) return '';
var svc = new AccountLookupService();
var result = svc.queryAccount(accountNumber);
return result ? JSON.stringify(result) : '';
},
type: 'AccountLookupAjax'
});
Critical: Set client_callable: true on this Script Include. The server-side service Script Include does NOT get this flag. If you accidentally mark the service include as client-callable, it exposes your API logic to the browser — and if you forget to mark the Ajax include as client-callable, your GlideAjax call will silently fail.
---
Step 4: Catalog Client Script
Attach an onChange event to the account number variable. Type: A Catalog Item. Applies to your specific catalog item.
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return;
g_form.clearMessages();
g_form.setValue('customer_name', '');
g_form.setValue('account_type', '');
var accountNumber = (newValue || '').trim();
if (!accountNumber) return;
g_form.showFieldMsg('account_number', 'Searching...', 'info');
var ga = new GlideAjax('AccountLookupAjax');
ga.addParam('sysparm_name', 'lookupAccount');
ga.addParam('sysparm_account_number', accountNumber);
ga.getXMLAnswer(function(answer) {
g_form.hideFieldMsg('account_number', true);
if (!answer) {
alert('No account exists.');
return;
}
var result;
try { result = JSON.parse(answer); } catch(e) {
alert('Unable to parse response.');
return;
}
if (result.found) {
g_form.setValue('customer_name', result.name || '');
g_form.setValue('account_type', result.account_type || '');
g_form.showFieldMsg('account_number', 'Account found.', 'info');
} else {
alert('No account exists.');
}
});
}
Don't fire on load. The if (isLoading) return; guard prevents the script from running when the form first renders with an existing value — which would trigger an API call the user didn't initiate.
---
Step 5: Routing
On the catalog item record, set the Group field to your fulfillment group. This populates assignment_group on the generated RITM when the request is submitted. No flow required for basic routing — the group field handles it.
---
Debugging Checklist
If the lookup fires but nothing populates:
1. Script Include not client-callable — check the client_callable flag on the Ajax bridge
2. Wrong sysparm_name — ga.addParam('sysparm_name', 'lookupAccount') must match the method name exactly
3. Parameter name mismatch — ga.addParam('sysparm_account_number', ...) and this.getParameter('sysparm_account_number') must be identical
4. REST Message function not named correctly — new sn_ws.RESTMessageV2('Name', 'FunctionName') — both strings must exactly match the records
5. OAuth token not fetched — first call to a new OAuth REST Message may fail. Open the REST Message record and use the "Test" button to force the initial token exchange
6. API returning non-200 — check response.getStatusCode() before parsing; a 401 or 403 looks like an empty result at the GlideAjax layer