---
title: "ServiceNow REST API Integration Guide for Developers (2026)"
description: "A complete ServiceNow REST API guide for developers building product integrations. Covers per-instance OAuth, Table API endpoints, sysparm query syntax, webhooks, rate limits, and working Python examples."
source_url: "https://www.getknit.dev/blog/servicenow-rest-api-integration-guide"
page_type: "blog"
---

_This is an educational blog post from Knit's blog: “ServiceNow REST API Integration Guide for Developers (2026)”._

# ServiceNow REST API Integration Guide for Developers (2026)

**Building a ServiceNow integration is fundamentally different from every other API integration you've built — because there is no single ServiceNow. Every customer runs their own instance at a unique subdomain, with their own OAuth endpoints, their own permission model, and their own table customisations. Guides written for ServiceNow developers working inside an instance won't help you. This one is written for developers building a product that connects to their customers' ServiceNow instances.**

> **Quick answer:** Use the **Table API** (`/api/now/table/{tableName}`) for reading and writing incidents, users, groups, and requests. Authenticate via **OAuth 2.0** — but collect the customer's instance URL first, since every OAuth endpoint is instance-specific. The five tables that cover 90% of ITSM product integration use cases are `incident`, `sys_user`, `sys_user_group`, `sc_request`, and `change_request`.

This guide covers per-instance OAuth setup, Table API endpoints and query syntax, webhook configuration, rate limits, and three real-world integration patterns with working code — all from the perspective of an external developer connecting to a customer's ServiceNow instance.

If your product needs to support ServiceNow alongside other ITSM tools like Jira, Zendesk, or GitHub Issues, there's a unified approach worth knowing about — covered in the [Building with Knit](https://getknit.dev/blog/servicenow-rest-api-integration-guide#) section.

## The ServiceNow API: Table API, Scripted REST, and Import Sets

ServiceNow exposes several API surfaces. The right one for your integration depends on what you're doing:

| What you want to do | Recommended approach |
| --- | --- |
| Read/write incidents, users, groups, requests in real time | **Table API** (`/api/now/table/`) |
| Receive real-time event notifications from ServiceNow | **Business Rules + Outbound REST Messages** |
| Bulk import large datasets into ServiceNow | **Import Set API** (`/api/now/import/`) |
| Expose custom endpoints inside ServiceNow | **Scripted REST API** (requires ServiceNow dev access) |
| Query complex relationships across tables | **Table API with `sysparm_query`** |
| Retrieve specific records by sys\_id | **Table API GET by sys\_id** |

**The Table API** is the right choice for the vast majority of product integrations. It provides CRUD access to any ServiceNow table through a consistent URL pattern:

```
https://{instance}.service-now.com/api/now/table/{tableName}
```

The Scripted REST API requires a ServiceNow developer to create custom endpoints inside the instance — you can't deploy these from outside. The Import Set API is for bulk historical data loads, not real-time integrations.

## Authentication: Per-Instance OAuth and Why It's Different

ServiceNow OAuth is standard OAuth 2.0 in mechanics, but the endpoints are not standard — they're instance-specific. This is the detail that trips most developers up when building a multi-tenant integration.

For a typical API (Slack, GitHub, [HubSpot](https://md.getknit.dev/mcp-servers/hubspot-mcp-server)), you hardcode a single OAuth endpoint:

```
https://slack.com/api/oauth.v2.access
```

For ServiceNow, every customer has their own:

```
https://{customer-instance}.service-now.com/oauth_token.do
https://{customer-instance}.service-now.com/oauth_auth.do
```

This means your integration must:

1.  Collect the customer's ServiceNow instance URL before initiating OAuth
2.  Construct the OAuth endpoints dynamically per customer
3.  Store per-customer OAuth credentials (access token, refresh token, instance URL)
4.  Handle token refresh per customer independently

Here's what that looks like in practice:

### Step 1: Collect the Instance URL

Your onboarding UI needs to ask for the instance identifier — the `[company]` part of `https://[company].service-now.com`. This is what Knit's auth screen shows users when they connect ServiceNow.

```
def get_servicenow_endpoints(instance: str) -> dict:
    """
    Build instance-specific OAuth endpoints from the instance identifier.
    instance = "mycompany" (not the full URL)
    """
    base = f"https://{instance}.service-now.com"
    return {
        "base_url": base,
        "auth_url": f"{base}/oauth_auth.do",
        "token_url": f"{base}/oauth_token.do",
    }
```

### Step 2: Register an OAuth Provider in ServiceNow

Before any OAuth flow can happen, the customer's ServiceNow admin must register your application as an OAuth provider in their instance: **System OAuth > Application Registry > New > Create an OAuth API endpoint for external clients**.

Required fields:

*   **Name:** Your application name
*   **Client ID:** Auto-generated (give this to the customer)
*   **Client Secret:** Auto-generated (store securely)
*   **Redirect URL:** Your callback URL

This is a one-time admin step per customer instance. Document it clearly in your onboarding instructions.

### Step 3: OAuth Authorization Flow

```
import requests
from urllib.parse import urlencode

def get_auth_url(instance: str, client_id: str, redirect_uri: str, state: str) -> str:
    endpoints = get_servicenow_endpoints(instance)
    params = {
        "response_type": "code",
        "client_id": client_id,
        "redirect_uri": redirect_uri,
        "state": state  # CSRF protection — always validate on callback
    }
    return f"{endpoints['auth_url']}?{urlencode(params)}"

def exchange_code_for_tokens(instance: str, client_id: str, client_secret: str,
                              code: str, redirect_uri: str) -> dict:
    endpoints = get_servicenow_endpoints(instance)
    response = requests.post(
        endpoints["token_url"],
        data={
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": redirect_uri,
            "client_id": client_id,
            "client_secret": client_secret
        }
    )
    response.raise_for_status()
    tokens = response.json()
    # Store tokens["access_token"], tokens["refresh_token"], and instance per customer
    return tokens
```

### Step 4: Token Refresh

ServiceNow access tokens expire after **30 minutes** by default (configurable by the admin). Build refresh logic before you hit your first expiry:

```
def refresh_access_token(instance: str, client_id: str, client_secret: str,
                          refresh_token: str) -> dict:
    endpoints = get_servicenow_endpoints(instance)
    response = requests.post(
        endpoints["token_url"],
        data={
            "grant_type": "refresh_token",
            "client_secret": client_secret,
            "client_id": client_id,
            "refresh_token": refresh_token
        }
    )
    response.raise_for_status()
    return response.json()  # New access_token and refresh_token
```

> If you're building a product that integrates with ServiceNow alongside other ITSM tools — Jira, Zendesk, GitHub, Linear — building and maintaining per-instance OAuth for each one is significant infrastructure overhead. Knit handles ServiceNow's instance URL collection and OAuth flow per customer, so you get a single integration layer across all your supported tools. → [getknit.dev/integration/servicenow](https://getknit.dev/integration/servicenow)

### The Tables

ServiceNow has hundreds of tables. For a B2B product integration, these five cover the vast majority of use cases:

| Table name | What it contains | Common use cases |
| --- | --- | --- |
| `incident` | IT incidents and support tickets | Sync tickets, create incidents from your product, update status |
| `sys_user` | All users in the instance | User lookup, assignee resolution, member sync |
| `sys_user_group` | Teams and groups | Group-based routing, access control mapping |
| `sc_request` | Service catalog requests | Read service requests submitted by users |
| `change_request` | Change management records | Monitor or create change requests |

All Table API requests follow the same pattern:

```
GET https://{instance}.service-now.com/api/now/table/{table}
Authorization: Bearer {access_token}
Accept: application/json
Content-Type: application/json
X-no-response-body: false
```

### The sysparm Parameters

ServiceNow's Table API uses `sysparm_` prefixed query parameters for filtering, field selection, and pagination. Understanding these is essential — without them you'll either pull the entire table or struggle with pagination.

| Parameter | Purpose | Example |
| --- | --- | --- |
| `sysparm_query` | Filter records using encoded query syntax | `state=1^assigned_toISNOTEMPTY` |
| `sysparm_fields` | Return only specific fields (comma-separated) | `sys_id,number,short_description,state` |
| `sysparm_limit` | Max records per page (default: 10, max: 10,000) | `100` |
| `sysparm_offset` | Pagination offset | `100` (page 2 with limit 100) |
| `sysparm_display_value` | Return display values instead of raw values | `true` or `all` |
| `sysparm_exclude_reference_link` | Remove reference links from response (smaller payload) | `true` |

### Reading Incidents

```
def get_incidents(instance: str, token: str,
                  state: str = None, assigned_to: str = None,
                  limit: int = 100, offset: int = 0) -> dict:
    """
    Fetch incidents from a ServiceNow instance.
    state codes: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed
    """
    query_parts = []
    if state:
        query_parts.append(f"state={state}")
    if assigned_to:
        query_parts.append(f"assigned_to.user_name={assigned_to}")

    params = {
        "sysparm_limit": limit,
        "sysparm_offset": offset,
                          "resolved_at,sys_created_on,sys_updated_on",
        "sysparm_exclude_reference_link": "true",
        "sysparm_display_value": "false"  # Raw values are easier to work with
    }
    if query_parts:
        params["sysparm_query"] = "^".join(query_parts)

    response = requests.get(
        f"https://{instance}.service-now.com/api/now/table/incident",
        headers={
            "Authorization": f"Bearer {token}",
        },
        params=params
    )
    response.raise_for_status()

    # Pagination: check X-Total-Count header for total record count
    total = int(response.headers.get("X-Total-Count", 0))
    return {
        "records": response.json()["result"],
        "total": total,
        "has_more": (offset + limit) < total
    }
```

### Creating an Incident

```
def create_incident(instance: str, token: str,
                    short_description: str, description: str,
                    caller_id: str = None, priority: int = 3,
                    assignment_group: str = None) -> dict:
    """
    Creates an incident. Priority: 1=Critical, 2=High, 3=Moderate, 4=Low.
    caller_id and assignment_group are sys_id values from sys_user/sys_user_group.
    """
    payload = {
        "short_description": short_description,
        "description": description,
        "priority": str(priority),
        "impact": str(priority),    # Often mirrors priority
        "urgency": str(priority)
    }
    if caller_id:
        payload["caller_id"] = caller_id
    if assignment_group:
        payload["assignment_group"] = assignment_group

    response = requests.post(
        f"https://{instance}.service-now.com/api/now/table/incident",
        headers={
            "Authorization": f"Bearer {token}",
            "Accept": "application/json",
        },
        json=payload
    )
    response.raise_for_status()
    result = response.json()["result"]
    return {
        "sys_id": result["sys_id"],       # Use this for future updates
        "number": result["number"],        # Human-readable e.g. INC0012345
        "state": result["state"],
    }
```

### Updating an Incident

```
def update_incident(instance: str, token: str,
                    sys_id: str, **fields) -> dict:
    """
    Update any incident fields by sys_id.
    Common fields: state, assigned_to, assignment_group, work_notes, close_notes
    """
    response = requests.patch(
        f"https://{instance}.service-now.com/api/now/table/incident/{sys_id}",
        headers={
            "Authorization": f"Bearer {token}",
            "Accept": "application/json",
        },
        json=fields
    )
    response.raise_for_status()
    return response.json()["result"]
```

### Users and Groups

```
# Get a user by their email address (common lookup pattern)
def get_user_by_email(instance: str, token: str, email: str) -> dict | None:
    response = requests.get(
        f"https://{instance}.service-now.com/api/now/table/sys_user",
        headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
        params={
            "sysparm_query": f"email={email}^active=true",
            "sysparm_fields": "sys_id,name,email,user_name",
            "sysparm_limit": 1,
        }
    )
    response.raise_for_status()
    results = response.json()["result"]
    return results[0] if results else None

# List all active groups
def list_groups(instance: str, token: str) -> list:
    response = requests.get(
        f"https://{instance}.service-now.com/api/now/table/sys_user_group",
        headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
        params={
            "sysparm_query": "active=true",
            "sysparm_fields": "sys_id,name,description,manager",
            "sysparm_limit": 1000,
        }
    )
    response.raise_for_status()
    return response.json()["result"]
```

## Webhooks: Business Rules and Outbound REST Messages

ServiceNow does not have native outbound webhooks that you configure from outside the instance. Real-time event notifications require a ServiceNow admin on the customer side to set up two things: a **Business Rule** (which triggers on record events) and an **Outbound REST Message** (which sends the payload to your server).

This is a key difference from APIs like GitHub or Slack where you register a webhook URL programmatically. For ServiceNow, you need to provide your customers' IT teams with setup instructions.

**What the customer's admin configures:**

**Business Rule** (System Definition > Business Rules):

*   Table: `incident`
*   When to run: `after` insert/update
*   Condition: (whatever triggers the notification — e.g., state changes)
*   Script:

```
// ServiceNow Business Rule script
var message = new sn_ws.RESTMessageV2('Your Integration', 'POST incident');
message.setStringParameterNoEscape('sys_id', current.sys_id);
message.setStringParameterNoEscape('number', current.number);
message.setStringParameterNoEscape('state', current.state);
message.setStringParameterNoEscape('updated_at', current.sys_updated_on);
var response = message.execute();
```

**Outbound REST Message** (System Web Services > Outbound > REST Message):

*   Endpoint: your server's webhook URL
*   HTTP Method: POST
*   Authentication: Basic or OAuth (your server's credentials)

**On your server**, receive and process the payload:

```
from flask import Flask, request, abort
import hmac, hashlib

app = Flask(__name__)

@app.route("/webhook/servicenow", methods=["POST"])
def handle_servicenow_event():
    # ServiceNow doesn't send a standard signature header —
    # secure your endpoint via IP allowlisting or a shared secret
    # passed as a query param or custom header agreed with the admin
    payload = request.json
    sys_id = payload.get("sys_id")
    state = payload.get("state")

    # State codes: 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed
    if state in ("6", "7"):
        close_linked_item_in_your_product(sys_id)

    return "", 200
```

Because webhook setup requires admin access on the customer's instance, build your integration to work without webhooks first (polling) and offer webhook setup as an enhancement for customers whose admins can configure it.

## Rate Limits

ServiceNow rate limits are **instance-configured**, not globally fixed — your customer's IT admin controls them. This creates a situation you won't face with other APIs: two customers on the same plan can have different rate limits.

| Configuration | Value |
| --- | --- |
| Default rate limit | ~5,000 requests/hour per user account (instance-configured) |
| Default max records per query | 10,000 records (governed by `glide.db.max_view_records`, adjustable by admin) |
| Max `sysparm_limit` per request | 10,000 |
| Token expiry (default) | 30 minutes |
| Rate limit headers | Not returned — watch for `429 Too Many Requests` |
| Response format | JSON (default) or XML |

Unlike GitHub or Slack, ServiceNow does not return rate limit headers (`X-RateLimit-Remaining` etc.) on every response. You'll receive a `429 Too Many Requests` when you hit the limit — build retry logic with exponential backoff:

```
import time

def servicenow_request(url: str, token: str, max_retries: int = 3, **kwargs) -> requests.Response:
    for attempt in range(max_retries):
        response = requests.get(url, headers={
            "Authorization": f"Bearer {token}",
        }, **kwargs)

        if response.status_code == 429:
            wait = 2 ** attempt * 10  # 10s, 20s, 40s
            time.sleep(wait)
            continue

        if response.status_code == 401:
            # Token likely expired — trigger refresh and retry once
            raise TokenExpiredError("Access token expired")

        response.raise_for_status()
        return response

    raise Exception(f"Max retries exceeded for {url}")
```

For sustained high-volume integrations, use a dedicated integration user account in ServiceNow rather than a human user's account — this ensures your rate limit isn't shared with the user's other API activity.

### Pattern 1: Sync Incidents into Your Product

Pull all open incidents and keep them in sync with periodic polling:

```
def full_incident_sync(instance: str, token: str) -> list:
    """
    Full sync of all open and in-progress incidents.
    Run on initial connection; switch to delta sync (updatedAfter) for ongoing.
    """
    all_incidents = []
    offset = 0
    limit = 100

    while True:
        page = get_incidents(
            instance=instance,
            token=token,
            limit=limit,
            offset=offset
        )
        all_incidents.extend(page["records"])

        if not page["has_more"]:
            break
        offset += limit

    # Normalise ServiceNow state codes to your product's status model
    status_map = {
        "1": "open", "2": "in_progress", "3": "on_hold",
    }

    return [
        {
            "external_id": i["sys_id"],
            "reference": i["number"],
            "title": i["short_description"],
            "status": status_map.get(str(i["state"]), "unknown"),
            "priority": i["priority"],
            "assignee_id": i.get("assigned_to"),
            "created_at": i["sys_created_on"],
            "updated_at": i["sys_updated_on"]
        }
        for i in all_incidents
    ]
```

### Pattern 2: Create an Incident from Your Product

The common "escalate to IT" pattern — a user triggers an action in your product and it creates a ServiceNow incident:

**Raw ServiceNow approach** — you need to resolve the user's sys\_id first, look up the right assignment group sys\_id, then create the incident:

```
# Step 1: resolve caller sys_id from user's email
caller = get_user_by_email(instance, token, user_email)
caller_sys_id = caller["sys_id"] if caller else None

# Step 2: look up assignment group sys_id
groups = list_groups(instance, token)
group = next((g for g in groups if g["name"] == "IT Help Desk"), None)
group_sys_id = group["sys_id"] if group else None

# Step 3: create the incident
incident = create_incident(
    instance=instance,
    token=token,
    short_description=f"Alert from {your_product}: {alert_title}",
    description=alert_details,
    caller_id=caller_sys_id,
    assignment_group=group_sys_id,
    priority=2  # High
)
# Store incident["sys_id"] in your DB for future status sync
```

**With** [**Knit**](https://getknit.dev/) — skip the sys\_id resolution steps. Knit's normalised endpoints return consistent IDs you can use directly:

```
# Get incidents already filtered and paginated
incidents = requests.get(
    "https://api.getknit.dev/v1.0/ticketing/tickets.list",
    headers={
        "Authorization": f"Bearer {knit_token}",
        "X-Knit-Integration-Id": integration_id
    },
    params={"status": "OPEN", "assignedToId": user_id}
)
# Update an incident's status
requests.post(
    "https://api.getknit.dev/v1.0/ticketing/ticket.update",
    headers={
        "Authorization": f"Bearer {knit_token}",
        "X-Knit-Integration-Id": integration_id
    },
    json={"ticketId": ticket_id, "status": "IN_PROGRESS", "assignedToId": agent_id}
)
```

### Pattern 3: User and Group Sync for Access Control

Many products need to know which ServiceNow users and groups a customer has, to map them to your product's access model:

```
def sync_users_and_groups(instance: str, token: str) -> dict:
    """
    Sync all active users and groups from ServiceNow.
    Used to populate assignee pickers and map access levels.
    """
    # Fetch users — paginate if the instance has many
    users_response = requests.get(
        f"https://{instance}.service-now.com/api/now/table/sys_user",
        headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
        params={
            "sysparm_query": "active=true",
            "sysparm_fields": "sys_id,name,email,user_name,department",
            "sysparm_limit": 1000,
        }
    )
    users = users_response.json()["result"]

    # Fetch groups
    groups = list_groups(instance, token)

    return {
        "users": [
            {"id": u["sys_id"], "name": u["name"],
             "email": u["email"], "username": u["user_name"]}
            for u in users
        ],
        "groups": [
            {"id": g["sys_id"], "name": g["name"]}
            for g in groups
        ]
    }
```

## Building ServiceNow Integrations with Knit

The two hardest parts of a ServiceNow product integration are both auth-related: collecting the instance URL from each customer, constructing per-instance OAuth endpoints, and managing token refresh independently per customer installation. These are real engineering problems that have nothing to do with the value you're delivering to users.

Knit handles ServiceNow authentication — including instance URL collection and per-customer OAuth — so your integration starts from a normalised API call rather than an auth infrastructure build. The same Knit headers work across all your ticketing integrations:

```
Authorization: Bearer {your-knit-token}
X-Knit-Integration-Id: {customer-integration-id}
```

This is especially valuable if your product also supports Jira, Zendesk, GitHub Issues, Linear, or Asana — Knit's same API surface covers all of them, so you write the integration logic once.

The Knit APIs available for ServiceNow:

| Knit API | Endpoint | Maps to in ServiceNow | Use cases |
| --- | --- | --- | --- |
| [Get Tickets](https://developers.getknit.dev/reference/get-tickets) | `GET /ticketing/tickets.list` | `incident` table | List incidents with 11 filters: accountId, contactId, assignedToId, status, ticketType, date ranges |
| [Update Ticket](https://developers.getknit.dev/reference/update-ticket) | `POST /ticketing/ticket.update` | `incident` PATCH | Update status, assignee, priority, group, due date |
| [Get Accounts](https://developers.getknit.dev/reference/get-accounts-ticketing) | `GET /ticketing/accounts` | Customer accounts / companies | List accounts linked to incidents |
| [Get Account By Id](https://developers.getknit.dev/reference/get-account-ticketing-id) | `GET /ticketing/account?accountId=` | Single account record | Fetch account details for a specific incident |
| [Get Contacts](https://developers.getknit.dev/reference/get-contacts-ticketing) | `GET /ticketing/contacts` | `sys_user` (contact view) | List contacts/callers with email and phone |
| [Get Contact By Id](https://developers.getknit.dev/reference/get-contact-ticketing-id) | `GET /ticketing/contact?contactId=` | Single contact record | Returns id, name, email, phone, accountId |
| [Get Users](https://developers.getknit.dev/reference/get-users-ticketing) | `GET /ticketing/users?accountId=` | `sys_user` (agent view) | Build assignee picker; map to your user directory |
| [Get User By Id](https://developers.getknit.dev/reference/get-user-ticketing-id) | `GET /ticketing/user?userId=` | Single `sys_user` | Resolve a specific user for display or routing |
| [Get Groups](https://developers.getknit.dev/reference/get-groups-ticketing) | `GET /ticketing/groups?accountId=` | `sys_user_group` | List assignment groups; map to your access model |
| [Get Group By Id](https://developers.getknit.dev/reference/get-group-ticketing-id) | `GET /ticketing/group?groupId=` | Single group record | Fetch group details for routing or display |

**Example: list open high-priority incidents via Knit**

/

```
import requests

def get_open_high_priority_incidents(knit_token: str, integration_id: str) -> list:
    """
    No instance URL handling. No token refresh. No sysparm syntax.
    Works the same way for ServiceNow, Jira, Zendesk, and every other Knit-supported tool.
    """
    all_tickets = []
    cursor = None

    while True:
        params = {"status": "OPEN"}
        if cursor:
            params["cursor"] = cursor

        response = requests.get(
            "https://api.getknit.dev/v1.0/ticketing/tickets.list",
            headers={
                "Authorization": f"Bearer {knit_token}",
                "X-Knit-Integration-Id": integration_id
            },
            params=params
        )
        response.raise_for_status()
        data = response.json()["data"]
        all_tickets.extend(data["tickets"])

        cursor = data["pagination"].get("next")
        if not cursor:
            break

    return all_tickets
```

→ See the full ServiceNow integration on Knit: [getknit.dev/integration/servicenow](https://getknit.dev/integration/servicenow)

[ ](https://getknit.dev/integration/servicenow)→ Knit's ticketing API docs: [developers.getknit.dev](https://developers.getknit.dev/)

## What to Build First

1.  **Build your instance URL collection UI** — a simple input field asking for the ServiceNow instance identifier. This unlocks everything else. Document clearly what format you expect (`mycompany`, not `https://mycompany.service-now.com`).
2.  **Write your dynamic OAuth endpoint constructor** — a utility function that builds token and auth URLs from the instance identifier. Every other piece of your auth layer depends on this.
3.  **Prepare your onboarding documentation for customer admins** — ServiceNow OAuth requires the customer's IT admin to register your application. Write a clear step-by-step guide before any customer goes through onboarding.
4.  **Build token storage with per-customer isolation** — access token, refresh token, instance URL, and expiry time per customer. Implement token refresh before your first expiry, not after.
5.  **Implement incident list and create endpoints** — these cover the primary use case for 80%+ of ServiceNow integrations. Use `sysparm_fields` from the start to avoid pulling data you don't need.
6.  **Build user and group sync** — fetch `sys_user` and `sys_user_group` on integration setup and cache the results. These change infrequently and are needed to populate assignee pickers and resolve group names.
7.  **Add delta sync for incident updates** — poll `incident` with `sysparm_query=sys_updated_on>javascript:gs.dateGenerate('YYYY-MM-DD','HH:mm:ss')` to fetch only records changed since your last sync rather than re-pulling everything.
8.  **Document the webhook setup process** — provide your customers' admins with a Business Rule + Outbound REST Message template they can deploy, enabling real-time sync without polling.

## Summary

| Topic | Key fact |
| --- | --- |
| **Primary API** | Table API: `https://{instance}.service-now.com/api/now/table/{tableName}` |
| **Auth approach** | OAuth 2.0 — but endpoints are per-instance, not global |
| **Token expiry** | 30 minutes by default — build refresh logic before first use |
| **Key tables** | `incident`, `sys_user`, `sys_user_group`, `sc_request`, `change_request` |
| **State codes (incident)** | 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed |
| **Filtering** | `sysparm_query` with ServiceNow encoded query syntax |
| **Max records per query** | 10,000 (default) — paginate with `sysparm_offset` |
| **Rate limits** | Instance-configured (typically ~5,000 req/hr) — no standard headers |
| **Webhooks** | Business Rules + Outbound REST Messages — requires customer admin |
| **Multi-integration shortcut** | Knit handles instance URL collection, OAuth, and normalises across Jira, Zendesk, GitHub, and more |

## Frequently Asked Questions

**What is the ServiceNow Table API?**

The ServiceNow Table API is the primary REST interface for reading and writing records across any ServiceNow table. It exposes endpoints at `https://{instance}.service-now.com/api/now/table/{tableName}` and supports GET, POST, PUT, PATCH, and DELETE operations. For product integrations, the most relevant tables are `incident`, `sys_user`, `sys_user_group`, `sc_request`, and `change_request`. The Table API supports powerful query filtering via the `sysparm_query` parameter.

**How do I authenticate with the ServiceNow REST API?**

ServiceNow supports OAuth 2.0 (recommended for production) and Basic Auth. For OAuth, the token endpoint is `https://{instance}.service-now.com/oauth_token.do` and the authorization endpoint is `https://{instance}.service-now.com/oauth_auth.do` — both are instance-specific, so you must collect the customer's instance URL before initiating the OAuth flow. Tokens expire after 30 minutes by default; use the refresh token to obtain new ones without user interaction.

**What is sysparm\_query in ServiceNow?**

`sysparm_query` is the ServiceNow Table API's parameter for filtering records. It uses ServiceNow's encoded query syntax: field operators joined with `^` (AND) or `^OR` (OR). Common operators include `=`, `!=`, `IN`, `STARTSWITH`, `CONTAINS`. Example: `state=1^assigned_toISNOTEMPTY^opened_at>=javascript:gs.beginningOfLast30Days()`. Build queries in the ServiceNow Filter Builder UI first, then copy the encoded query string to use in your API calls.

**What are the ServiceNow API rate limits?**

ServiceNow API rate limits are configured per instance by the customer's admin, not fixed globally. The default is typically 5,000 API requests per hour per user account, but enterprise instances can have this set differently. ServiceNow does not return standard rate limit headers on every response — watch for `429 Too Many Requests` and implement exponential backoff. The API defaults to a maximum of 10,000 records per single Table API query (controlled by the `glide.db.max_view_records` system property — most instances leave this at the default).

**How do ServiceNow webhooks work?**

ServiceNow does not have native outbound webhooks that you register from outside the instance. Real-time event notifications are built using Business Rules (server-side scripts that fire on table record events) combined with Outbound REST Messages. This requires a ServiceNow admin on the customer's side to configure. For integrations where webhook setup isn't feasible, use delta polling: query the incident table with a `sys_updated_on>` filter on a schedule.

**What is the difference between the ServiceNow Table API and Import Set API?**

The Table API directly reads and writes records with immediate effect — the right choice for most product integrations. The Import Set API stages data in a temporary table first, then a transform map processes it into the target table. Use Import Sets only for bulk historical data migration. For real-time integrations involving incidents, users, and groups, always use the Table API.

**Which ServiceNow tables should I use for an ITSM integration?**

Focus on five tables: `incident` for IT incidents, `sys_user` for user records, `sys_user_group` for team assignments, `sc_request` for service catalog requests, and `change_request` for change management. The `incident` table's `state` field uses numeric codes — 1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed — always map these explicitly in your code rather than relying on display values.

**Is there a simpler way to integrate with ServiceNow without building per-instance OAuth for each customer?**

Yes. Knit provides a unified ticketing API that handles ServiceNow authentication — including collecting the instance URL and managing the per-instance OAuth flow per customer. Instead of building dynamic OAuth endpoint logic, token refresh, and per-customer credential storage, your customers connect their ServiceNow instance once through Knit's auth layer. You then call Knit's normalised endpoints for incidents, accounts, contacts, users, and groups — the same interface that works across Jira, GitHub, Zendesk, and more. → [getknit.dev/integration/servicenow](https://getknit.dev/integration/servicenow)


## Related pages

- [How Knit works](https://md.getknit.dev/how-knit-works)
- [Unified API product](https://md.getknit.dev/products/unified-api)
