RESTMessageV2 in ServiceNow: Complete Guide to Outbound REST Calls

RESTMessageV2 is the server-side API for making outbound HTTP requests from ServiceNow scripts — calling external REST APIs — including the Table API — from Business Rules, Script Includes, Scheduled Jobs, and Flow Designer Script steps. This guide covers every aspect: the basic request patterns, all HTTP methods, authentication, named REST messages, async execution, error handling, and the production patterns that make integrations reliable.

When to use RESTMessageV2

RESTMessageV2 is appropriate when you need fine-grained control over the HTTP request from a script. For most Flow Designer integrations, use IntegrationHub spokes or the REST action — they handle authentication and error handling with less code. Use RESTMessageV2 directly in Script Includes when:

  • The integration logic is complex and better expressed as code than as flow steps
  • You are wrapping an API in a Script Include that flows or Business Rules call
  • You need programmatic control over headers, parameters, or body construction
  • You are building a reusable integration library for other developers to use

Basic GET request

var rm = new sn_ws.RESTMessageV2();
rm.setHttpMethod('GET');
rm.setEndpoint('https://api.example.com/v1/users');
rm.setRequestHeader('Authorization', 'Bearer ' + token);
rm.setRequestHeader('Content-Type', 'application/json');
rm.setRequestHeader('Accept', 'application/json');

var response = rm.execute();
var statusCode = response.getStatusCode();
var body = response.getBody();

if (statusCode == 200) {
    var data = JSON.parse(body);
    gs.log('Users returned: ' + data.users.length, 'APIIntegration');
} else {
    gs.error('API GET failed: ' + statusCode + ' — ' + body, 'APIIntegration');
}

POST request with JSON body

var rm = new sn_ws.RESTMessageV2();
rm.setHttpMethod('POST');
rm.setEndpoint('https://api.example.com/v1/incidents');
rm.setRequestHeader('Content-Type', 'application/json');
rm.setRequestHeader('Authorization', 'Bearer ' + getToken());

var payload = {
    title: current.getValue('short_description'),
    severity: current.getValue('urgency'),
    reference: current.getValue('number'),
    assignee: current.assigned_to.getDisplayValue()
};

rm.setRequestBody(JSON.stringify(payload));

var response = rm.execute();
if (response.getStatusCode() == 201) {
    var created = JSON.parse(response.getBody());
    gs.log('External ticket created: ' + created.id, 'TicketSync');
    current.setValue('u_external_id', created.id);
}

PUT request — full resource update

var rm = new sn_ws.RESTMessageV2();
rm.setHttpMethod('PUT');
rm.setEndpoint('https://api.example.com/v1/incidents/' + externalId);
rm.setRequestHeader('Content-Type', 'application/json');
rm.setRequestHeader('Authorization', 'Bearer ' + token);
rm.setRequestBody(JSON.stringify({
    status: 'resolved',
    resolution: current.getValue('resolution_notes'),
    resolved_by: gs.getUser().getFullName(),
    resolved_at: new GlideDateTime().getValue()
}));
var response = rm.execute();
gs.log('PUT status: ' + response.getStatusCode(), 'TicketSync');

PATCH request — partial update

var rm = new sn_ws.RESTMessageV2();
rm.setHttpMethod('PATCH');
rm.setEndpoint('https://api.example.com/v1/tickets/' + ticketId);
rm.setRequestHeader('Content-Type', 'application/json');
rm.setRequestHeader('Authorization', 'Bearer ' + token);
// Only send the fields that changed
rm.setRequestBody(JSON.stringify({ status: 'in_progress' }));
var response = rm.execute();

DELETE request

var rm = new sn_ws.RESTMessageV2();
rm.setHttpMethod('DELETE');
rm.setEndpoint('https://api.example.com/v1/subscriptions/' + subscriptionId);
rm.setRequestHeader('Authorization', 'Bearer ' + token);
var response = rm.execute();
if (response.getStatusCode() == 204) {
    gs.log('Subscription deleted: ' + subscriptionId, 'SubSync');
}

Query parameters

var rm = new sn_ws.RESTMessageV2();
rm.setHttpMethod('GET');
rm.setEndpoint('https://api.example.com/v1/incidents');
// Query parameters in the URL
rm.setEndpoint('https://api.example.com/v1/incidents?status=open&priority=high&limit=50');
// Or use setQueryParameter()
rm.setQueryParameter('status', 'open');
rm.setQueryParameter('priority', 'high');
rm.setQueryParameter('limit', '50');

Named REST Messages — the reusable configuration approach

For integrations used in multiple places, create a named REST Message record at System Web Services > Outbound > REST Message. Define the base URL, authentication, and HTTP methods once — then call them by name from any script. This externalises credentials and endpoint URLs from code.

// Call a named REST Message with a named HTTP Method
var rm = new sn_ws.RESTMessageV2('Jira Integration', 'Create Issue');
// Set template variable values defined in the named message
rm.setStringParameterNoEscape('project_key', 'OPS');
rm.setStringParameterNoEscape('summary', current.getValue('short_description'));
rm.setStringParameterNoEscape('priority', getPriorityLabel(current.getValue('priority')));

var response = rm.execute();
if (response.getStatusCode() == 201) {
    var result = JSON.parse(response.getBody());
    current.setValue('u_jira_key', result.key);
}

Named REST Messages work with Connection and Credential Aliases — credentials are never in the code, only in the secure credential store.

Timeout configuration

// Set connection timeout (how long to wait to establish connection)
rm.setHttpTimeout(30000); // 30 seconds in milliseconds

// For slow external APIs, increase the timeout
rm.setHttpTimeout(60000); // 60 seconds

Asynchronous execution

Use executeAsync() when the REST call is triggered from a Business Rule or other synchronous context and you do not want to block the save operation:

// executeAsync returns immediately — response handled via callback
var rm = new sn_ws.RESTMessageV2('External API', 'Notify');
rm.setStringParameterNoEscape('incident_number', current.getValue('number'));
rm.executeAsync(); // Returns a response object for later inspection, does not block

// For true fire-and-forget from a Business Rule:
// Use an Async Business Rule, or better, a Flow Designer flow

For most async integration patterns, Flow Designer with a record trigger is cleaner than executeAsync() from a Business Rule. See Flow Designer vs Business Rules.

Response methods — reading what came back

var response = rm.execute();

// HTTP status code
var status = response.getStatusCode(); // e.g. 200, 201, 400, 500

// Response body as string
var body = response.getBody();

// Parse JSON response
var data = JSON.parse(body);

// Response headers
var contentType = response.getHeader('Content-Type');
var rateLimitRemaining = response.getHeader('X-RateLimit-Remaining');

// All headers as a map
var headers = response.getHeaders();

Production error handling pattern

Wrap every RESTMessageV2 call in try/catch — network failures throw exceptions, they do not return a response object:

function callExternalAPI(payload) {
    try {
        var rm = new sn_ws.RESTMessageV2();
        rm.setHttpMethod('POST');
        rm.setEndpoint(gs.getProperty('my.integration.endpoint', ''));
        rm.setRequestHeader('Content-Type', 'application/json');
        rm.setRequestHeader('Authorization', 'Bearer ' + getToken());
        rm.setRequestBody(JSON.stringify(payload));
        rm.setHttpTimeout(30000);

        var response = rm.execute();
        var status = response.getStatusCode();
        var body = response.getBody();

        if (status >= 200 && status < 300) {
            return { success: true, data: JSON.parse(body) };
        } else if (status == 429) {
            gs.warn('Rate limited by external API', 'ExternalAPI');
            return { success: false, error: 'rate_limited', retryable: true };
        } else if (status >= 500) {
            gs.error('External API server error: ' + status, 'ExternalAPI');
            return { success: false, error: 'server_error', retryable: true };
        } else {
            gs.error('External API client error: ' + status + ' — ' + body, 'ExternalAPI');
            return { success: false, error: 'client_error', retryable: false };
        }

    } catch (e) {
        gs.error('RESTMessageV2 exception: ' + e.getMessage(), 'ExternalAPI');
        return { success: false, error: 'network_error', retryable: true };
    }
}

HTTP status code handling reference

For the complete HTTP status code reference and how to handle each one in ServiceNow integrations, see HTTP status codes in ServiceNow integrations.

Authentication patterns

// Bearer token (OAuth)
rm.setRequestHeader('Authorization', 'Bearer ' + accessToken);

// Basic auth (not recommended — use named REST message with credential alias instead)
var credentials = gs.base64Encode(username + ':' + password);
rm.setRequestHeader('Authorization', 'Basic ' + credentials);

// API Key header
rm.setRequestHeader('X-API-Key', apiKey);
rm.setRequestHeader('api-key', apiKey); // vendor-specific header names

// API Key in query parameter
rm.setQueryParameter('api_key', apiKey);

For OAuth 2.0 flows, see the OAuth 2.0 guide. For storing credentials securely, see Connection and Credential Aliases.

Related guides:

Testing and debugging RESTMessageV2 calls

Debugging outbound REST calls in ServiceNow requires a systematic approach. Start with the simplest possible test — a GET to a known working endpoint with no authentication — and add complexity from there. Use Scripts - Background to test RESTMessageV2 code interactively before embedding it in a Business Rule or Script Include:

// Test in Scripts - Background first
var rm = new sn_ws.RESTMessageV2();
rm.setHttpMethod('GET');
rm.setEndpoint('https://httpbin.org/get'); // public test endpoint
rm.setHttpTimeout(10000);
try {
    var response = rm.execute();
    gs.log('Status: ' + response.getStatusCode());
    gs.log('Body: ' + response.getBody().substring(0, 500));
} catch(e) {
    gs.log('Error: ' + e.getMessage());
}

Common debugging sequence: first verify the endpoint is reachable from the platform (use httpbin.org or a simple public API). Then add authentication. Then add your specific headers and body. Each step isolates potential problems. If your instance uses a MID Server for internal API calls, test with and without the MID Server to isolate routing issues from authentication issues.

Rate limiting and retry strategy

External APIs have rate limits. Production integrations that process high volumes of records — syncing 5,000 incidents to an external ticketing system, for example — will hit rate limits if they make synchronous API calls inside a GlideRecord loop. The correct architecture:

// WRONG — synchronous calls in loop (will hit rate limits)
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^u_synced=false');
gr.query();
while (gr.next()) {
    callExternalAPI(gr); // immediate, sequential calls
}

// RIGHT — queue for processing with rate-aware job
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^u_synced=false');
gr.query();
while (gr.next()) {
    // Add to a sync queue table
    var queue = new GlideRecord('u_sync_queue');
    queue.setValue('u_incident', gr.getUniqueValue());
    queue.setValue('u_status', 'pending');
    queue.insert();
}
// Separate Scheduled Job processes queue at controlled rate

The Scheduled Job that processes the queue can implement rate limiting by adding a wait between API calls or limiting the batch size per execution. This pattern also handles retry logic cleanly — failed items stay in the queue with an error status and get retried on the next job run.

Integration with Flow Designer

For integrations triggered by record events (new incident created, change approved, user onboarded), Flow Designer with a REST step or IntegrationHub spoke is often cleaner than RESTMessageV2 in a Business Rule. Use RESTMessageV2 directly when: the integration logic requires complex scripting that Flow Designer cannot express cleanly, you are building a reusable Script Include integration library, or the integration is triggered from a scheduled job rather than a record event.

Related: OAuth 2.0 · Credential Aliases · HTTP status codes · MID Server · IntegrationHub spokes

Production integration architecture patterns

Well-architected ServiceNow integrations separate the HTTP mechanics (handled by RESTMessageV2) from the business logic (handled by a Script Include) and the retry/error management (handled by a queue table and Scheduled Job). This three-layer pattern makes integrations testable, maintainable, and resilient. The Script Include wraps RESTMessageV2 and returns a structured result object. The caller (Business Rule, Flow Designer, Scheduled Job) decides what to do with the result. Failed calls are logged to a queue for retry rather than silently dropped or causing the triggering operation to fail. This is the pattern used in enterprise ServiceNow implementations where integration reliability is a business requirement, not a nice-to-have. When you are asked to review an existing integration or architect a new one, this three-layer separation is the first question to ask: is the HTTP layer separate from the business logic layer? If not, that is the first refactor to plan.

Mastering RESTMessageV2 is what separates ServiceNow practitioners who can only configure within the platform from those who can connect it to anything. Combined with OAuth 2.0, Credential Aliases, and Scheduled Jobs, you have a complete outbound integration toolkit.

The complete integrations reference

The NowSpectrum Integrations Complete Reference Guide covers REST API, SOAP, IntegrationHub, MID Server, OAuth 2.0, and 50 interview Q&As in 26 pages.

Get the Integrations Guide →
← Back to all posts