QuotaGuard and ServiceNow Integration Guide
QuotaGuard and ServiceNow Integration Guide
QuotaGuard gives your integration two fixed outbound IP addresses from one environment variable. Set QUOTAGUARDSTATIC_URL, route your ServiceNow REST API calls through it, and every request reaches ServiceNow from one of your two static IPs, so it passes an instance that restricts access by source IP. This guide covers the ServiceNow REST API (the Table API and other REST endpoints). It does not cover MID Server connectivity, which uses a different model.
Why ServiceNow needs a static IP
ServiceNow lets an administrator restrict instance access by source IP, and that restriction applies to API traffic, not just human logins. When it is active, a request from an address that is not on the allowed list is refused even when the credentials are valid. An integration running on a cloud platform egresses from an IP that changes on every deploy, scale event, and sometimes every request, so there is no stable address to register. A static outbound IP is the fixed address you register once and never revisit.
This affects any integration that has to reach an IP-restricted ServiceNow instance, including:
- iPaaS and workflow tools (Workato, Mulesoft, Boomi, n8n, Make, Zapier) calling the Table API
- Custom apps and microservices on Heroku, Render, Railway, Fly.io, AWS Lambda, Google Cloud Run, or Azure Functions
- Data-sync and ETL jobs pulling incident, CMDB, or user data on a schedule
- AI agents that open, update, or look up ServiceNow tickets
- CI and serverless functions that touch ServiceNow during a pipeline
Where ServiceNow’s IP restriction is configured
This is set on the ServiceNow side by an instance administrator, not in QuotaGuard. Two controls apply, and an instance may use either or both. Add your two QuotaGuard IPs to whichever is in force.
- IP Address Access Control (
All > System Security > IP Address Access Control). Allow and Deny rules by IP range across the whole instance. ServiceNow blocks an address only when a Deny rule matches and no Allow rule matches, and Allow rules supersede Deny rules. The common pattern is to allow your trusted ranges, then add a Deny-all rule (0.0.0.0to255.255.255.255). ServiceNow will not let an admin insert a rule that would lock out their own current address. The IP Range Based Authentication plugin (com.snc.ipauthenticator) backs this and is present by default on modern instances. - REST API Access Policies (
All > System Web Services > API Access Policies). Authentication policies attached here can restrict access by IP, role, or group, scoped to specific REST APIs rather than the whole instance. This is the granular option when you want only your integration’s APIs IP-locked while the rest of the instance stays open.
Some instances instead migrate these into Adaptive Authentication (IP filter criteria). The effect for you is identical: register both QuotaGuard IPs in the allowed set and your integration’s traffic is accepted.
Getting Started
After creating a QuotaGuard account, you are redirected to your dashboard, where you can find your proxy credentials and two static IP addresses.
Choose the right proxy region: Select the QuotaGuard region closest to where your application runs to minimize latency. The region is set at sign-up. Changes after sign-up require contacting support. QuotaGuard runs in 11 AWS regions: US-East-1 (N. Virginia), US-West-2 (Oregon), CA-Central-1 (Montreal), EU-West-1 (Ireland), EU-West-2 (London), EU-Central-1 (Frankfurt), AP-Northeast-1 (Tokyo), AP-Southeast-1 (Singapore), AP-Southeast-2 (Sydney), AP-South-1 (Mumbai), and SA-East-1 (Sao Paulo).
Step 1: Set the proxy
Store your QuotaGuard connection string as an environment variable on the host that runs your integration. The proxy host is region-specific, so copy the exact value from your dashboard.
QUOTAGUARDSTATIC_URL="http://username:password@<your-quotaguard-proxy-host>:9293"
Keep it in the environment, not in source control, alongside your ServiceNow credentials. If your platform has a secret store (Fly secrets, Heroku config vars, AWS Secrets Manager, and so on), use it rather than a plaintext variable.
ServiceNow authentication, in brief
The REST Table API lives at https://{instance}.service-now.com/api/now/table/{tableName} and supports GET, POST, PUT, PATCH, and DELETE. It accepts:
- OAuth 2.0, recommended for production. Request a token from
https://{instance}.service-now.com/oauth_token.do(the authorization endpoint ishttps://{instance}.service-now.com/oauth_auth.do). Inbound OAuth supports the password and authorization_code grants. Access tokens are short-lived (the ServiceNow default is around 30 minutes) and refresh tokens are long-lived; both are configurable inSystem OAuth > Application Registry. - Basic Auth, the username and password of a ServiceNow user. Simplest to start with. Note that newer ServiceNow releases can restrict inbound Basic Auth through API Access Policies, so confirm it is permitted on your instance, or use OAuth.
- Mutual auth and inbound API keys are also supported on current releases.
Whichever you use, the rule for this guide is the same: route both the auth call and the data call through the proxy so every request to ServiceNow leaves from your static IPs.
Step 2: Route your ServiceNow calls through the proxy
Attach the proxy to the requests that reach ServiceNow. The examples below use Basic Auth and the incident table for brevity. Swap in your Authorization: Bearer <token> header if you use OAuth, and route the token request through the same proxy.
curl
curl -x "$QUOTAGUARDSTATIC_URL" \
-u "$SNOW_USER:$SNOW_PASSWORD" \
-H "Accept: application/json" \
"https://your-instance.service-now.com/api/now/table/incident?sysparm_limit=1"
Python (requests)
import os, requests
proxies = {
"http": os.environ["QUOTAGUARDSTATIC_URL"],
"https": os.environ["QUOTAGUARDSTATIC_URL"],
}
resp = requests.get(
"https://your-instance.service-now.com/api/now/table/incident",
params={"sysparm_limit": 1},
auth=(os.environ["SNOW_USER"], os.environ["SNOW_PASSWORD"]),
headers={"Accept": "application/json"},
proxies=proxies,
)
print(resp.status_code, resp.json())
Python (httpx)
import os, httpx
with httpx.Client(proxy=os.environ["QUOTAGUARDSTATIC_URL"]) as client:
resp = client.get(
"https://your-instance.service-now.com/api/now/table/incident",
params={"sysparm_limit": 1},
auth=(os.environ["SNOW_USER"], os.environ["SNOW_PASSWORD"]),
headers={"Accept": "application/json"},
)
print(resp.status_code)
Node.js (node-fetch)
Node’s built-in fetch ignores a proxy agent. Use node-fetch with https-proxy-agent, which honors it.
const fetch = require('node-fetch');
const { HttpsProxyAgent } = require('https-proxy-agent');
const agent = new HttpsProxyAgent(process.env.QUOTAGUARDSTATIC_URL);
const auth = Buffer.from(`${process.env.SNOW_USER}:${process.env.SNOW_PASSWORD}`).toString('base64');
const resp = await fetch(
'https://your-instance.service-now.com/api/now/table/incident?sysparm_limit=1',
{ agent, headers: { Authorization: `Basic ${auth}`, Accept: 'application/json' } },
);
console.log(resp.status, await resp.json());
Node.js (axios)
axios honors a proxy most reliably through an agent rather than its proxy option for HTTPS targets.
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const agent = new HttpsProxyAgent(process.env.QUOTAGUARDSTATIC_URL);
const resp = await axios.get(
'https://your-instance.service-now.com/api/now/table/incident',
{
params: { sysparm_limit: 1 },
auth: { username: process.env.SNOW_USER, password: process.env.SNOW_PASSWORD },
httpsAgent: agent,
proxy: false,
},
);
console.log(resp.status);
Go (net/http)
package main
import (
"fmt"
"net/http"
"net/url"
"os"
)
func main() {
proxyURL, _ := url.Parse(os.Getenv("QUOTAGUARDSTATIC_URL"))
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
}
req, _ := http.NewRequest("GET",
"https://your-instance.service-now.com/api/now/table/incident?sysparm_limit=1", nil)
req.SetBasicAuth(os.Getenv("SNOW_USER"), os.Getenv("SNOW_PASSWORD"))
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status)
}
Ruby (Net::HTTP)
require "net/http"
require "uri"
proxy = URI.parse(ENV["QUOTAGUARDSTATIC_URL"])
target = URI.parse("https://your-instance.service-now.com/api/now/table/incident?sysparm_limit=1")
http = Net::HTTP.new(target.host, target.port,
proxy.host, proxy.port, proxy.user, proxy.password)
http.use_ssl = true
req = Net::HTTP::Get.new(target)
req.basic_auth(ENV["SNOW_USER"], ENV["SNOW_PASSWORD"])
req["Accept"] = "application/json"
puts http.request(req).code
Ruby (Faraday)
require "faraday"
conn = Faraday.new(
url: "https://your-instance.service-now.com",
proxy: ENV["QUOTAGUARDSTATIC_URL"],
)
resp = conn.get("/api/now/table/incident") do |req|
req.params["sysparm_limit"] = 1
req.headers["Accept"] = "application/json"
req.headers["Authorization"] =
"Basic " + ["#{ENV['SNOW_USER']}:#{ENV['SNOW_PASSWORD']}"].pack("m0")
end
puts resp.status
PHP (Guzzle)
<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$resp = $client->get(
'https://your-instance.service-now.com/api/now/table/incident',
[
'proxy' => getenv('QUOTAGUARDSTATIC_URL'),
'query' => ['sysparm_limit' => 1],
'auth' => [getenv('SNOW_USER'), getenv('SNOW_PASSWORD')],
'headers' => ['Accept' => 'application/json'],
]
);
echo $resp->getStatusCode();
PHP (cURL)
<?php
$ch = curl_init('https://your-instance.service-now.com/api/now/table/incident?sysparm_limit=1');
curl_setopt_array($ch, [
CURLOPT_PROXY => getenv('QUOTAGUARDSTATIC_URL'),
CURLOPT_USERPWD => getenv('SNOW_USER') . ':' . getenv('SNOW_PASSWORD'),
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE);
Java (java.net.http)
Proxy authentication with the built-in client is supplied through an Authenticator. Note that the JDK requires a system property to send proxy Basic credentials over HTTPS tunnels.
import java.net.*;
import java.net.http.*;
import java.util.Base64;
// Run with: -Djdk.http.auth.tunneling.disabledSchemes=""
URI proxyUri = URI.create(System.getenv("QUOTAGUARDSTATIC_URL"));
Authenticator auth = new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
String[] u = proxyUri.getUserInfo().split(":", 2);
return new PasswordAuthentication(u[0], u[1].toCharArray());
}
};
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress(proxyUri.getHost(), proxyUri.getPort())))
.authenticator(auth)
.build();
String snow = Base64.getEncoder().encodeToString(
(System.getenv("SNOW_USER") + ":" + System.getenv("SNOW_PASSWORD")).getBytes());
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://your-instance.service-now.com/api/now/table/incident?sysparm_limit=1"))
.header("Authorization", "Basic " + snow)
.header("Accept", "application/json")
.build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.statusCode());
.NET (HttpClient)
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var proxyUri = new Uri(Environment.GetEnvironmentVariable("QUOTAGUARDSTATIC_URL"));
var userInfo = proxyUri.UserInfo.Split(':', 2);
var handler = new HttpClientHandler
{
Proxy = new WebProxy(proxyUri.GetLeftPart(UriPartial.Authority))
{
Credentials = new NetworkCredential(userInfo[0], userInfo[1])
},
UseProxy = true
};
using var client = new HttpClient(handler);
var snow = Convert.ToBase64String(Encoding.ASCII.GetBytes(
$"{Environment.GetEnvironmentVariable("SNOW_USER")}:{Environment.GetEnvironmentVariable("SNOW_PASSWORD")}"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", snow);
var resp = await client.GetAsync(
"https://your-instance.service-now.com/api/now/table/incident?sysparm_limit=1");
Console.WriteLine((int)resp.StatusCode);
Elixir (Req)
proxy_url = System.get_env("QUOTAGUARDSTATIC_URL")
snow = Base.encode64("#{System.get_env("SNOW_USER")}:#{System.get_env("SNOW_PASSWORD")}")
Req.get!(
"https://your-instance.service-now.com/api/now/table/incident",
params: [sysparm_limit: 1],
headers: [{"authorization", "Basic #{snow}"}, {"accept", "application/json"}],
connect_options: [proxy: proxy_url]
)
|> Map.get(:status)
|> IO.inspect()
Elixir (HTTPoison)
uri = URI.parse(System.get_env("QUOTAGUARDSTATIC_URL"))
[user, pass] = String.split(uri.userinfo, ":")
snow = Base.encode64("#{System.get_env("SNOW_USER")}:#{System.get_env("SNOW_PASSWORD")}")
opts = [
proxy: {String.to_charlist(uri.host), uri.port},
proxy_auth: {String.to_charlist(user), String.to_charlist(pass)}
]
{:ok, resp} = HTTPoison.get(
"https://your-instance.service-now.com/api/now/table/incident?sysparm_limit=1",
[{"Authorization", "Basic #{snow}"}, {"Accept", "application/json"}],
opts
)
IO.inspect(resp.status_code)
Rust (reqwest)
use reqwest::Proxy;
use std::env;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let proxy = Proxy::all(&env::var("QUOTAGUARDSTATIC_URL")?)?;
let client = reqwest::Client::builder().proxy(proxy).build()?;
let resp = client
.get("https://your-instance.service-now.com/api/now/table/incident?sysparm_limit=1")
.basic_auth(env::var("SNOW_USER")?, Some(env::var("SNOW_PASSWORD")?))
.header("Accept", "application/json")
.send()
.await?;
println!("{}", resp.status());
Ok(())
}
OAuth token request through the proxy (Python)
If you use OAuth, route the token call through the proxy too, so it also leaves from your static IPs.
import os, requests
proxies = {"https": os.environ["QUOTAGUARDSTATIC_URL"]}
token = requests.post(
"https://your-instance.service-now.com/oauth_token.do",
data={
"grant_type": "password",
"client_id": os.environ["SNOW_CLIENT_ID"],
"client_secret": os.environ["SNOW_CLIENT_SECRET"],
"username": os.environ["SNOW_USER"],
"password": os.environ["SNOW_PASSWORD"],
},
proxies=proxies,
).json()["access_token"]
resp = requests.get(
"https://your-instance.service-now.com/api/now/table/incident",
headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
proxies=proxies,
)
print(resp.status_code)
Inbound: a fixed IP for ServiceNow to call your app
The reverse direction is covered too. If you configure ServiceNow outbound REST messages, business rules, or Flow Designer actions to call your application, and your app runs behind a firewall or on a platform with rotating IPs, QuotaGuard’s static inbound entry point gives ServiceNow one permanent address to reach, which the receiving side allowlists once. Inbound is configured from your dashboard on Micro plans and above. Contact support if you want help wiring an inbound endpoint for ServiceNow callbacks.
Testing
After configuring the proxy, confirm the IP your traffic uses. Route a request to the IP check endpoint through the proxy:
curl -x "$QUOTAGUARDSTATIC_URL" https://ip.quotaguard.com
Expected response:
{"ip":"<one of your two QuotaGuard static IPs>"}
The returned IP must be one of the two static IPs in your dashboard. Call it more than once and you will see both, because traffic is load-balanced across the pair. Register both in ServiceNow.
Application-level test
Confirm the proxy is wired into your own code, not just your shell, with a small endpoint that reports the egress IP your app actually uses.
import os, requests
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/test-proxy")
def test_proxy():
proxies = {"https": os.environ["QUOTAGUARDSTATIC_URL"]}
ip = requests.get("https://ip.quotaguard.com", proxies=proxies).json()["ip"]
return jsonify({"static_ip": ip, "proxy_configured": True})
If the IP it reports is one of your two static IPs, your ServiceNow calls will leave from the same pair.
Latency Considerations
Routing through QuotaGuard adds one network hop. For ServiceNow REST calls, which are not latency-critical, this is rarely a factor. Pick the QuotaGuard region closest to where your integration runs to keep it minimal.
| Configuration | Added latency |
|---|---|
| Same region (app and proxy co-located) | 10-20 ms |
| Cross-region | 50-100 ms |
Every request uses the same two static IPs regardless of where your app scales, so one allowlist entry pair covers every region you run in. That is the advantage over standing up a separate static-egress IP per region.
Self-managed alternatives (honest, demoted)
ServiceNow has no native feature that gives your application a static egress IP, because the IP that matters is your side, not theirs. You can get one without QuotaGuard by running your integration on a VM with a reserved IP, or by routing a serverless workload through a NAT gateway with a reserved address. Both work and are the right choice for some architectures. They also carry real weight: a VM is yours to patch, monitor, and keep available, and a NAT gateway adds base hourly and per-GB charges plus the VPC plumbing to route through it, and it only fixes the egress for workloads inside that one network. QuotaGuard is the managed version. It gives you two IPs that work across every region and every other platform you run, supports inbound as well as outbound, and adds nothing to your network. Choose whichever fits. This page documents the QuotaGuard path.
Troubleshooting
- 407 Proxy Authentication Required. The proxy credentials are missing or wrong. Confirm
QUOTAGUARDSTATIC_URLincludes the username and password exactly as shown in your dashboard, and that your client is actually sending them. In Java, this is also the symptom of the disabled tunneling scheme: run with-Djdk.http.auth.tunneling.disabledSchemes="". - ServiceNow 403 or 401 with valid credentials. The call authenticated but the source IP is not allowed. Confirm both QuotaGuard IPs are in the instance’s IP Address Access Control allow set or the relevant REST API Access Policy, and that no Deny rule overrides them. Allow rules supersede Deny rules in ServiceNow.
- Worked from your laptop, fails once deployed. Classic rotating-egress symptom. Your laptop’s IP happened to be allowed, or the instance was open when you tested. The deployed host egresses from a different, rotating address. Route it through the proxy and register the two static IPs.
- Wrong IP returned by the test. Your request is not going through the proxy. Make sure the proxy is attached to the specific client or request, not just exported in a shell your runtime does not inherit. In axios, set
proxy: falseand usehttpsAgent, or the built-in proxy handling can interfere. - OAuth token request fails or returns 401. Confirm the OAuth application registry entry exists, the user is active and not locked out, and the grant type matches (password or authorization_code). Route the token call through the proxy as well, or ServiceNow sees it from a non-allowlisted IP.
- Basic Auth rejected on a newer instance. Recent ServiceNow releases can restrict inbound Basic Auth via API Access Policies. Switch to OAuth, or have an admin confirm Basic Auth is permitted for your API.
- Connection timeout. Confirm the destination host and port are reachable and that you registered both IPs. Registering only one is a common cause of intermittent failures, since traffic can leave from either address at any time.
- Intermittent failures, roughly half of requests. Almost always only one of the two IPs is allowlisted. Add both.
QuotaGuard Static vs QuotaGuard Shield
| Feature | QuotaGuard Static | QuotaGuard Shield |
|---|---|---|
| Protocol | HTTP / HTTPS / SOCKS5 | HTTPS / SOCKS5 over TLS |
| Customer-to-proxy hop | Plaintext | TLS-encrypted |
| HTTPS payload | Tunneled end-to-end, never decrypted at the proxy | Tunneled end-to-end, never decrypted at the proxy |
| Best for | Most apps | Regulated data or environments that require TLS on every hop |
| Starting price | $19/month | $29/month |
Static is right for most ServiceNow integrations: your calls are outbound HTTPS, tunneled through a CONNECT tunnel without decryption. Choose Shield if the workload handles regulated data or the environment requires TLS between your app and the proxy itself.
Ready to Get Started?
Get in touch or create a free trial account.
Read: How to Give Your Integration a Static IP for ServiceNow