scripted-rest-apis について
ServiceNow の開発者、インテグレーション担当者、管理者向けに、カスタム Scripted REST API の構築を解説するスキルです。エンドポイント設計、リクエストとレスポンス、セキュリティ、バージョン管理、テストを扱います。
主な機能
- 01カスタム REST エンドポイント設計
- 02リクエストとレスポンスの整形
- 03認証とアクセス制御
- 04クエリ・パスパラメーター処理
- 05エラー処理と API バージョン管理
- 06API Explorer と curl によるテスト
活用シーン
- 01外部システム向けの専用エンドポイントを公開
- 02API 操作に独自のビジネスロジックを実装
- 03複数テーブルのデータを集約
- 04アクセス制御付きのバージョン管理 API を定義
MCP 対応
ServiceNow のアプリケーションコンテキスト設定や API 定義・操作の作成、検索、更新に MCP ツールを使う手順を示します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ happy-technologies-llc/ happy-platform-skills --skill scripted-rest-apisインストール後、Claude Code で/scripted-rest-apisを入力して読み込まれたか確認します。
詳細
スクリプト型 REST API の開発
概要
このスキルでは、ServiceNow でカスタムのスクリプト型 REST API を開発するための包括的なガイドを提供します。スクリプト型 REST API を使用すると、次のことができます。
- カスタムエンドポイントの公開 - 標準の Table API では実現できない、目的に特化した API を作成します
- データ変換の制御 - 統合の要件に合わせてリクエスト/レスポンス形式を整えます
- ビジネスロジックの実装 - 1 回の API 呼び出しで複雑な処理を実行します
- 統合の保護 - きめ細かな認証と認可を適用します
- 外部システムのサポート - サードパーティシステムとの統合用インターフェースを提供します
スクリプト型 REST API を使用する場合:
- 標準の Table API では要件を満たせない
- API レイヤーにカスタムのビジネスロジックが必要
- 特定のリクエスト/レスポンス形式が必要
- 外部システムに専用のエンドポイントが必要
- 複数のテーブルからデータを集約する必要がある
対象者: カスタムの ServiceNow 統合を構築する開発者、統合スペシャリスト、管理者。
前提条件
- ロール:
admin、web_service_admin、またはスコープ付きアプリの開発者 - 権限: sys_ws_definition、sys_ws_operation の作成/変更
- 知識: JavaScript、REST の原則、HTTP メソッド、JSON
- 環境: 開発インスタンス(本番環境で API を直接開発しないでください)
- 関連スキル:
admin/update-set-management- API 定義の取り込みadmin/application-scope- スコープ付きアプリの API 開発security/acl-management- API のセキュリティ制御
テーブルアーキテクチャ
プログラムで API を作成するには、基盤となるテーブルを理解することが不可欠です。
コアテーブル
sys_ws_definition (API Definition)
|
+-- sys_ws_operation (Resources/Endpoints)
|
+-- Request Headers (configured in operation)
+-- Query Parameters (configured in operation)
+-- Path Parameters (defined in relative_path)
+-- Script (handles request/response)
主なリレーションシップ
| テーブル | 目的 | 主なフィールド |
|---|---|---|
sys_ws_definition | API コンテナー | name、namespace、base_path、active |
sys_ws_operation | 個々のエンドポイント | name、http_method、relative_path、script |
sys_script_include | 再利用可能なロジック | name、script、api_name |
sys_ws_api_header | カスタムヘッダー | name、api_id、direction |
API URL の構造
https://<instance>.service-now.com/api/<namespace>/<api_name>/<version>/<resource>/<path_param>
Example:
https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/12345
| | | | | |
Instance Namespace API Name Version Resource Path Param
手順
フェーズ 1:API の設計
API を作成する前に、構造を慎重に計画してください。
ステップ 1.1:API の要件を定義する
API 仕様を文書化します。
API Name: Customer Integration API
Namespace: x_company (scoped app) or now (global)
Version: v1
Base Path: /api/x_company/customer_api
Resources:
- GET /customers - List all customers (with pagination)
- GET /customers/{id} - Get specific customer
- POST /customers - Create new customer
- PUT /customers/{id} - Update customer
- DELETE /customers/{id} - Delete customer
- POST /customers/{id}/orders - Create order for customer
ステップ 1.2:認証を計画する
| 認証タイプ | 使用例 | 設定 |
|---|---|---|
| Basic Auth | サービスアカウント | デフォルトで有効 |
| OAuth 2.0 | サードパーティアプリ | OAuth プロバイダーを設定 |
| API Key | シンプルな統合 | カスタムヘッダーの検証 |
| Session | ブラウザベース | Cookie ベースの認証 |
| Mutual TLS | 高いセキュリティ | 証明書の検証 |
フェーズ 2:API 定義の作成
ステップ 2.1:コンテキストの設定(スコープ付きアプリ)
スコープ付きアプリで作成する場合:
MCP を使用:
Tool: SN-Set-Current-Application
Parameters:
app_sys_id: [your_app_sys_id]
Tool: SN-Set-Update-Set
Parameters:
update_set_sys_id: [your_update_set_sys_id]
ステップ 2.2:API 定義の作成
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_definition
data:
name: Customer Integration API
short_description: API for customer management and integration
namespace: x_company
doc_link: https://docs.company.com/api/customers
active: true
enforce_acl: true
is_versioned: true
baseline_version: v1
published: false
主なフィールドの説明:
| フィールド | 目的 | 推奨事項 |
|---|---|---|
namespace | URL のセグメント。通常はアプリのスコープ | アプリのスコープを使用します(x_company) |
enforce_acl | 基盤となるテーブルの ACL を確認 | セキュリティのため true に設定します |
is_versioned | API のバージョン管理を有効化 | 常に true に設定します |
baseline_version | バージョンが指定されていない場合のデフォルトバージョン | v1 から始めます |
published | API を検出可能にする | 開発中は false に設定します |
sys_id を保存します: api_definition_sys_id
ステップ 2.3:API 作成の確認
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_definition
query: name=Customer Integration API
fields: sys_id,name,namespace,active,service_address
service_address フィールドに API の完全な URL が表示されます。
フェーズ 3:リソース(オペレーション)の作成
ステップ 3.1:GET コレクションリソース(一覧)
ページネーション付きで顧客を一覧表示するエンドポイントを作成します。
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: List Customers
web_service_definition: [api_definition_sys_id]
http_method: GET
relative_path: /customers
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// Get query parameters for pagination and filtering
var limit = parseInt(request.queryParams.limit) || 20;
var offset = parseInt(request.queryParams.offset) || 0;
var active = request.queryParams.active;
var search = request.queryParams.q;
// Validate limit
if (limit > 100) limit = 100;
if (limit < 1) limit = 20;
// Build query
var gr = new GlideRecord('customer');
if (active !== undefined) {
gr.addQuery('active', active === 'true');
}
if (search) {
var qc = gr.addQuery('name', 'CONTAINS', search);
qc.addOrCondition('email', 'CONTAINS', search);
qc.addOrCondition('account_number', 'CONTAINS', search);
}
// Get total count before pagination
var countGR = new GlideAggregate('customer');
countGR.addAggregate('COUNT');
if (active !== undefined) {
countGR.addQuery('active', active === 'true');
}
countGR.query();
var totalCount = 0;
if (countGR.next()) {
totalCount = parseInt(countGR.getAggregate('COUNT'));
}
// Apply pagination
gr.orderBy('name');
gr.chooseWindow(offset, offset + limit);
gr.query();
// Build response
var customers = [];
while (gr.next()) {
customers.push({
sys_id: gr.getUniqueValue(),
name: gr.getValue('name'),
email: gr.getValue('email'),
account_number: gr.getValue('account_number'),
active: gr.getValue('active') === 'true',
created: gr.getValue('sys_created_on'),
updated: gr.getValue('sys_updated_on')
});
}
// Set response
response.setStatus(200);
response.setBody({
result: customers,
meta: {
total: totalCount,
limit: limit,
offset: offset,
has_more: (offset + limit) < totalCount
}
});
})(request, response);
short_description: Retrieve list of customers with pagination
requires_acl_authorization: true
requires_authentication: true
requires_snc_internal_role: false
sys_id を保存します: list_customers_operation_id
ステップ 3.2:GET 単一リソース
特定の顧客を取得するエンドポイントを作成します。
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Get Customer
web_service_definition: [api_definition_sys_id]
http_method: GET
relative_path: /customers/{id}
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// Get path parameter
var customerId = request.pathParams.id;
// Validate input
if (!customerId) {
response.setStatus(400);
response.setBody({
error: {
message: 'Customer ID is required',
code: 'MISSING_PARAMETER'
}
});
return;
}
// Query customer
var gr = new GlideRecord('customer');
// Support both sys_id and account_number lookup
if (customerId.length === 32) {
gr.get(customerId);
} else {
gr.addQuery('account_number', customerId);
gr.query();
gr.next();
}
if (!gr.isValidRecord()) {
response.setStatus(404);
response.setBody({
error: {
message: 'Customer not found',
code: 'NOT_FOUND',
detail: 'No customer exists with ID: ' + customerId
}
});
return;
}
// Check ACL (optional if enforce_acl is true on definition)
if (!gr.canRead()) {
response.setStatus(403);
response.setBody({
error: {
message: 'Access denied',
code: 'FORBIDDEN'
}
});
return;
}
// Build detailed response
var customer = {
sys_id: gr.getUniqueValue(),
name: gr.getValue('name'),
email: gr.getValue('email'),
phone: gr.getValue('phone'),
account_number: gr.getValue('account_number'),
address: {
street: gr.getValue('street'),
city: gr.getValue('city'),
state: gr.getValue('state'),
zip: gr.getValue('zip'),
country: gr.getValue('country')
},
contacts: [],
active: gr.getValue('active') === 'true',
created: gr.getValue('sys_created_on'),
updated: gr.getValue('sys_updated_on'),
created_by: gr.getDisplayValue('sys_created_by'),
updated_by: gr.getDisplayValue('sys_updated_by')
};
// Get related contacts
var contacts = new GlideRecord('customer_contact');
contacts.addQuery('customer', gr.getUniqueValue());
contacts.query();
while (contacts.next()) {
customer.contacts.push({
sys_id: contacts.getUniqueValue(),
name: contacts.getValue('name'),
email: contacts.getValue('email'),
phone: contacts.getValue('phone'),
primary: contacts.getValue('primary') === 'true'
});
}
response.setStatus(200);
response.setBody({ result: customer });
})(request, response);
short_description: Retrieve a specific customer by ID
requires_acl_authorization: true
requires_authentication: true
ステップ 3.3:POST リソース(作成)
新しい顧客を作成するエンドポイントを作成します。
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Create Customer
web_service_definition: [api_definition_sys_id]
http_method: POST
relative_path: /customers
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// Parse request body
var body = request.body.data;
// Validate required fields
var errors = [];
if (!body.name) errors.push('name is required');
if (!body.email) errors.push('email is required');
// Validate email format
if (body.email && !body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
errors.push('email format is invalid');
}
if (errors.length > 0) {
response.setStatus(400);
response.setBody({
error: {
message: 'Validation failed',
code: 'VALIDATION_ERROR',
details: errors
}
});
return;
}
// Check for duplicate email
var existing = new GlideRecord('customer');
existing.addQuery('email', body.email);
existing.query();
if (existing.next()) {
response.setStatus(409);
response.setBody({
error: {
message: 'Customer with this email already exists',
code: 'DUPLICATE_ERROR',
existing_id: existing.getUniqueValue()
}
});
return;
}
// Create customer
var gr = new GlideRecord('customer');
gr.initialize();
// Required fields
gr.setValue('name', body.name);
gr.setValue('email', body.email);
// Optional fields
if (body.phone) gr.setValue('phone', body.phone);
if (body.account_number) gr.setValue('account_number', body.account_number);
if (body.address) {
if (body.address.street) gr.setValue('street', body.address.street);
if (body.address.city) gr.setValue('city', body.address.city);
if (body.address.state) gr.setValue('state', body.address.state);
if (body.address.zip) gr.setValue('zip', body.address.zip);
if (body.address.country) gr.setValue('country', body.address.country);
}
gr.setValue('active', true);
// Check create permission
if (!gr.canCreate()) {
response.setStatus(403);
response.setBody({
error: {
message: 'Permission denied to create customer',
code: 'FORBIDDEN'
}
});
return;
}
var sysId = gr.insert();
if (sysId) {
// Set Location header for created resource
response.setHeader('Location', request.url + '/' + sysId);
response.setStatus(201);
response.setBody({
result: {
sys_id: sysId,
message: 'Customer created successfully'
}
});
} else {
response.setStatus(500);
response.setBody({
error: {
message: 'Failed to create customer',
code: 'INTERNAL_ERROR'
}
});
}
})(request, response);
short_description: Create a new customer
requires_acl_authorization: true
requires_authentication: true
ステップ 3.4:PUT リソース(更新)
既存の顧客を更新するエンドポイントを作成します。
MCP を使用する場合:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Update Customer
web_service_definition: [api_definition_sys_id]
http_method: PUT
relative_path: /customers/{id}
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var customerId = request.pathParams.id;
var body = request.body.data;
// Find customer
var gr = new GlideRecord('customer');
if (!gr.get(customerId)) {
response.setStatus(404);
response.setBody({
error: {
message: 'Customer not found',
code: 'NOT_FOUND'
}
});
return;
}
// Check update permission
if (!gr.canWrite()) {
response.setStatus(403);
response.setBody({
error: {
message: 'Permission denied to update customer',
code: 'FORBIDDEN'
}
});
return;
}
// Validate email if being changed
if (body.email && body.email !== gr.getValue('email')) {
if (!body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
response.setStatus(400);
response.setBody({
error: {
message: 'Invalid email format',
code: 'VALIDATION_ERROR'
}
});
return;
}
// Check for duplicate
var existing = new GlideRecord('customer');
existing.addQuery('email', body.email);
existing.addQuery('sys_id', '!=', customerId);
existing.query();
if (existing.next()) {
response.setStatus(409);
response.setBody({
error: {
message: 'Another customer with this email exists',
code: 'DUPLICATE_ERROR'
}
});
return;
}
}
// Update fields
var updatedFields = [];
if (body.name !== undefined) {
gr.setValue('name', body.name);
updatedFields.push('name');
}
if (body.email !== undefined) {
gr.setValue('email', body.email);
updatedFields.push('email');
}
if (body.phone !== undefined) {
gr.setValue('phone', body.phone);
updatedFields.push('phone');
}
if (body.active !== undefined) {
gr.setValue('active', body.active);
updatedFields.push('active');
}
if (body.address) {
if (body.address.street !== undefined) gr.setValue('street', body.address.street);
if (body.address.city !== undefined) gr.setValue('city', body.address.city);
if (body.address.state !== undefined) gr.setValue('state', body.address.state);
if (body.address.zip !== undefined) gr.setValue('zip', body.address.zip);
if (body.address.country !== undefined) gr.setValue('country', body.address.country);
updatedFields.push('address');
}
gr.update();
response.setStatus(200);
response.setBody({
result: {
sys_id: customerId,
message: 'Customer updated successfully',
updated_fields: updatedFields
}
});
})(request, response);
short_description: Update an existing customer
requires_acl_authorization: true
requires_authentication: true
ステップ 3.5:DELETE リソース
顧客を削除するエンドポイントを作成します。
MCP を使用する場合:
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Delete Customer
web_service_definition: [api_definition_sys_id]
http_method: DELETE
relative_path: /customers/{id}
operation_script: |
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var customerId = request.pathParams.id;
// Find customer
var gr = new GlideRecord('customer');
if (!gr.get(customerId)) {
response.setStatus(404);
response.setBody({
error: {
message: 'Customer not found',
code: 'NOT_FOUND'
}
});
return;
}
// Check delete permission
if (!gr.canDelete()) {
response.setStatus(403);
response.setBody({
error: {
message: 'Permission denied to delete customer',
code: 'FORBIDDEN'
}
});
return;
}
// Soft delete vs hard delete decision
// Option 1: Soft delete (preferred)
gr.setValue('active', false);
gr.update();
// Option 2: Hard delete (use with caution)
// gr.deleteRecord();
response.setStatus(200);
response.setBody({
result: {
sys_id: customerId,
message: 'Customer deactivated successfully'
}
});
// Alternative: Return 204 No Content for true DELETE
// response.setStatus(204);
})(request, response);
short_description: Delete (deactivate) a customer
requires_acl_authorization: true
requires_authentication: true
フェーズ 4:認証とセキュリティ
ステップ 4.1:API レベルの認証を設定する
API 定義で認証要件を制御します。
Tool: SN-Update-Record
Parameters:
table_name: sys_ws_definition
sys_id: [api_definition_sys_id]
data:
requires_authentication: true
enforce_acl: true
acl_failure_result: UNAUTHORIZED
ステップ 4.2:API キー認証を実装する
シンプルな連携の場合は、API キーの検証を実装します。
API キーの Script Include を作成する:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_include
data:
name: CustomerAPIAuth
script: |
var CustomerAPIAuth = Class.create();
CustomerAPIAuth.prototype = {
initialize: function() {
this.API_KEY_HEADER = 'X-API-Key';
this.API_KEY_TABLE = 'x_company_api_keys';
},
validateApiKey: function(request) {
var apiKey = request.getHeader(this.API_KEY_HEADER);
if (!apiKey) {
return {
valid: false,
error: 'API key required',
code: 'MISSING_API_KEY'
};
}
// Look up API key
var keyRecord = new GlideRecord(this.API_KEY_TABLE);
keyRecord.addQuery('key_value', apiKey);
keyRecord.addQuery('active', true);
keyRecord.addQuery('expires', '>', new GlideDateTime());
keyRecord.query();
if (!keyRecord.next()) {
return {
valid: false,
error: 'Invalid or expired API key',
code: 'INVALID_API_KEY'
};
}
// Update last used timestamp
keyRecord.setValue('last_used', new GlideDateTime());
keyRecord.update();
return {
valid: true,
client_id: keyRecord.getValue('client_id'),
permissions: keyRecord.getValue('permissions').split(','),
rate_limit: parseInt(keyRecord.getValue('rate_limit')) || 1000
};
},
type: 'CustomerAPIAuth'
};
api_name: CustomerAPIAuth
active: true
access: public
API オペレーションで使用する:
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// Validate API key
var auth = new CustomerAPIAuth();
var authResult = auth.validateApiKey(request);
if (!authResult.valid) {
response.setStatus(401);
response.setBody({
error: {
message: authResult.error,
code: authResult.code
}
});
return;
}
// Check permission
if (authResult.permissions.indexOf('read:customers') === -1) {
response.setStatus(403);
response.setBody({
error: {
message: 'Insufficient permissions',
code: 'FORBIDDEN',
required: 'read:customers'
}
});
return;
}
// Continue with request processing...
})(request, response);
ステップ 4.3:レート制限を実装する
API を保護するために、レート制限を追加します。
レートリミッターの Script Include を作成する:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_include
data:
name: APIRateLimiter
script: |
var APIRateLimiter = Class.create();
APIRateLimiter.prototype = {
initialize: function(clientId, rateLimit) {
this.clientId = clientId;
this.rateLimit = rateLimit || 100; // requests per minute
this.CACHE_PREFIX = 'api_rate_';
},
isAllowed: function() {
var cacheKey = this.CACHE_PREFIX + this.clientId;
var cache = new GlideSysCache('APIRateLimit');
// Get current count
var countStr = cache.get(cacheKey);
var count = countStr ? parseInt(countStr) : 0;
if (count >= this.rateLimit) {
return {
allowed: false,
remaining: 0,
reset_in: 60
};
}
// Increment count with 60 second TTL
cache.put(cacheKey, String(count + 1), 60);
return {
allowed: true,
remaining: this.rateLimit - count - 1,
limit: this.rateLimit
};
},
type: 'APIRateLimiter'
};
api_name: APIRateLimiter
active: true
API でレート制限を使用する:
// After authentication
var rateLimiter = new APIRateLimiter(authResult.client_id, authResult.rate_limit);
var rateResult = rateLimiter.isAllowed();
response.setHeader('X-RateLimit-Limit', rateResult.limit || authResult.rate_limit);
response.setHeader('X-RateLimit-Remaining', rateResult.remaining);
if (!rateResult.allowed) {
response.setStatus(429);
response.setHeader('Retry-After', rateResult.reset_in);
response.setBody({
error: {
message: 'Rate limit exceeded',
code: 'RATE_LIMITED',
retry_after: rateResult.reset_in
}
});
return;
}
フェーズ 5:クエリパラメーターとパスパラメーター
ステップ 5.1:一般的なクエリパラメーターのパターン
// Pagination
var limit = Math.min(parseInt(request.queryParams.limit) || 20, 100);
var offset = parseInt(request.queryParams.offset) || 0;
var page = parseInt(request.queryParams.page) || 1;
offset = (page - 1) * limit; // If using page instead of offset
// Sorting
var sortField = request.queryParams.sort || 'name';
var sortOrder = request.queryParams.order || 'asc';
var validSortFields = ['name', 'email', 'created', 'updated'];
if (validSortFields.indexOf(sortField) === -1) {
sortField = 'name';
}
if (sortOrder === 'desc') {
gr.orderByDesc(sortField);
} else {
gr.orderBy(sortField);
}
// Filtering
var filters = {
active: request.queryParams.active,
category: request.queryParams.category,
created_after: request.queryParams.created_after,
created_before: request.queryParams.created_before
};
if (filters.active !== undefined) {
gr.addQuery('active', filters.active === 'true');
}
if (filters.category) {
gr.addQuery('category', filters.category);
}
if (filters.created_after) {
gr.addQuery('sys_created_on', '>=', filters.created_after);
}
if (filters.created_before) {
gr.addQuery('sys_created_on', '<=', filters.created_before);
}
// Field selection
var fields = request.queryParams.fields;
if (fields) {
var fieldList = fields.split(',');
// Validate fields before use
var validFields = ['sys_id', 'name', 'email', 'phone', 'active'];
fieldList = fieldList.filter(function(f) {
return validFields.indexOf(f.trim()) !== -1;
});
}
// Search/Text filter
var searchQuery = request.queryParams.q || request.queryParams.search;
if (searchQuery) {
var qc = gr.addQuery('name', 'CONTAINS', searchQuery);
qc.addOrCondition('description', 'CONTAINS', searchQuery);
}
ステップ 5.2:パスパラメーターのパターン
// Single path parameter
// URL: /customers/{id}
var customerId = request.pathParams.id;
// Multiple path parameters
// URL: /customers/{customer_id}/orders/{order_id}
var customerId = request.pathParams.customer_id;
var orderId = request.pathParams.order_id;
// Validate path parameters
if (!customerId || customerId.length < 1) {
response.setStatus(400);
response.setBody({
error: {
message: 'Invalid customer ID',
code: 'INVALID_PARAMETER'
}
});
return;
}
// Support multiple ID formats
function resolveCustomerId(id) {
// sys_id (32 char hex)
if (/^[a-f0-9]{32}$/i.test(id)) {
return { field: 'sys_id', value: id };
}
// Account number (numeric or alphanumeric)
if (/^[A-Z]{2}\d{6}$/.test(id)) {
return { field: 'account_number', value: id };
}
// Email
if (id.indexOf('@') !== -1) {
return { field: 'email', value: id };
}
return null;
}
var idInfo = resolveCustomerId(customerId);
if (!idInfo) {
response.setStatus(400);
response.setBody({
error: {
message: 'Unrecognized ID format',
code: 'INVALID_ID_FORMAT',
supported_formats: ['sys_id', 'account_number (XX000000)', 'email']
}
});
return;
}
フェーズ 6:エラー処理とステータスコード
ステップ 6.1:標準的なエラーレスポンス形式
// Error response helper
function sendError(response, status, message, code, details) {
response.setStatus(status);
var error = {
error: {
message: message,
code: code,
timestamp: new GlideDateTime().getValue()
}
};
if (details) {
error.error.details = details;
}
response.setBody(error);
}
// Usage examples
sendError(response, 400, 'Invalid request body', 'BAD_REQUEST');
sendError(response, 401, 'Authentication required', 'UNAUTHORIZED');
sendError(response, 403, 'Permission denied', 'FORBIDDEN');
sendError(response, 404, 'Resource not found', 'NOT_FOUND');
sendError(response, 409, 'Conflict with existing resource', 'CONFLICT');
sendError(response, 422, 'Validation failed', 'VALIDATION_ERROR', errors);
sendError(response, 429, 'Rate limit exceeded', 'RATE_LIMITED');
sendError(response, 500, 'Internal server error', 'INTERNAL_ERROR');
sendError(response, 503, 'Service unavailable', 'SERVICE_UNAVAILABLE');
ステップ 6.2:包括的なエラー処理
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
try {
// Wrap entire request processing in try-catch
// Validate content type for POST/PUT
if (['POST', 'PUT', 'PATCH'].indexOf(request.getMethod()) !== -1) {
var contentType = request.getHeader('Content-Type');
if (!contentType || contentType.indexOf('application/json') === -1) {
response.setStatus(415);
response.setBody({
error: {
message: 'Content-Type must be application/json',
code: 'UNSUPPORTED_MEDIA_TYPE'
}
});
return;
}
}
// Parse body with error handling
var body;
try {
body = request.body.data;
if (!body && request.body.dataString) {
body = JSON.parse(request.body.dataString);
}
} catch (parseError) {
response.setStatus(400);
response.setBody({
error: {
message: 'Invalid JSON in request body',
code: 'PARSE_ERROR',
detail: parseError.message
}
});
return;
}
// Main processing logic here...
} catch (e) {
// Log error for debugging
gs.error('[CustomerAPI] Unhandled error: ' + e.message + '\nStack: ' + e.stack);
response.setStatus(500);
response.setBody({
error: {
message: 'An unexpected error occurred',
code: 'INTERNAL_ERROR',
reference: gs.generateGUID() // For support reference
}
});
}
})(request, response);
ステップ 6.3:HTTP ステータスコードのリファレンス
| ステータス | 意味 | 使用する状況 |
|---|---|---|
| 200 | OK | GET、PUT、PATCH、DELETE の成功 |
| 201 | 作成済み | 新しいリソースを作成する POST の成功 |
| 204 | コンテンツなし | レスポンスボディを伴わない DELETE の成功 |
| 400 | 不正なリクエスト | 構文が無効、または必須フィールドが不足 |
| 401 | 認証されていない | 認証情報がない、または無効 |
| 403 | 禁止 | 認証済みだが、権限がない |
| 404 | 見つからない | リソースが存在しない |
| 405 | メソッドが許可されていない | エンドポイントで HTTP メソッドがサポートされていない |
| 409 | 競合 | リソースの重複、または状態の競合 |
| 415 | サポートされていないメディアタイプ | Content-Type ヘッダーが正しくない |
| 422 | 処理できないエンティティ | 検証エラー |
| 429 | リクエストが多すぎる | レート制限を超過 |
| 500 | 内部サーバーエラー | 予期しないサーバーエラー |
| 503 | サービス利用不可 | 一時的なメンテナンス/過負荷 |
フェーズ 7:API のバージョン管理
ステップ 7.1:バージョンの設定
バージョン管理用に API 定義を更新する:
Tool: SN-Update-Record
Parameters:
table_name: sys_ws_definition
sys_id: [api_definition_sys_id]
data:
is_versioned: true
baseline_version: v1
deprecated_versions: v1-beta
ステップ 7.2:バージョン固有のオペレーション
バージョン固有のエンドポイントを作成します。
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: List Customers v2
web_service_definition: [api_definition_sys_id]
http_method: GET
relative_path: /v2/customers
operation_script: |
// V2 version with enhanced response format
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
// V2 includes additional fields and different structure
var customers = [];
var gr = new GlideRecord('customer');
gr.query();
while (gr.next()) {
customers.push({
id: gr.getUniqueValue(),
attributes: {
name: gr.getValue('name'),
email: gr.getValue('email'),
phone: gr.getValue('phone')
},
relationships: {
contacts: '/api/x_company/customer_api/v2/customers/' + gr.getUniqueValue() + '/contacts',
orders: '/api/x_company/customer_api/v2/customers/' + gr.getUniqueValue() + '/orders'
},
meta: {
created: gr.getValue('sys_created_on'),
updated: gr.getValue('sys_updated_on')
}
});
}
response.setStatus(200);
response.setBody({
data: customers,
links: {
self: request.url,
next: null,
prev: null
}
});
})(request, response);
ステップ 7.3:非推奨化ヘッダー
// Add deprecation headers for old versions
if (request.url.indexOf('/v1/') !== -1) {
response.setHeader('Deprecation', 'true');
response.setHeader('Sunset', 'Sat, 01 Jul 2027 00:00:00 GMT');
response.setHeader('Link', '</api/x_company/customer_api/v2/customers>; rel="successor-version"');
}
フェーズ 8:ドキュメントとテスト
ステップ 8.1:API Explorer との連携
ServiceNow は API Explorer のドキュメントを自動生成します。さらに充実させましょう。
Tool: SN-Update-Record
Parameters:
table_name: sys_ws_definition
sys_id: [api_definition_sys_id]
data:
doc_link: https://docs.company.com/api/customers
short_description: |
Customer management API for external integrations.
Authentication: Basic Auth or API Key (X-API-Key header)
Rate Limit: 100 requests per minute
Base URL: https://instance.service-now.com/api/x_company/customer_api/v1
ステップ 8.2:curl によるテスト
GET リクエスト:
# List customers
curl -X GET \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers?limit=10&offset=0" \
-H "Accept: application/json" \
-H "Authorization: Basic $(echo -n 'username:password' | base64)"
# Get specific customer
curl -X GET \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
-H "Accept: application/json" \
-H "X-API-Key: your-api-key-here"
POST リクエスト:
curl -X POST \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Basic $(echo -n 'username:password' | base64)" \
-d '{
"name": "Acme Corporation",
"email": "[email protected]",
"phone": "555-1234",
"address": {
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
}
}'
PUT リクエスト:
curl -X PUT \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Basic $(echo -n 'username:password' | base64)" \
-d '{
"name": "Acme Corporation Updated",
"phone": "555-5678"
}'
DELETE リクエスト:
curl -X DELETE \
"https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
-H "Accept: application/json" \
-H "Authorization: Basic $(echo -n 'username:password' | base64)"
ステップ 8.3:Postman によるテスト
Postman コレクションの構造:
{
"info": {
"name": "Customer API",
"description": "ServiceNow Customer Integration API"
},
"variable": [
{ "key": "base_url", "value": "https://dev12345.service-now.com" },
{ "key": "api_path", "value": "/api/x_company/customer_api/v1" }
],
"auth": {
"type": "basic",
"basic": [
{ "key": "username", "value": "{{username}}" },
{ "key": "password", "value": "{{password}}" }
]
},
"item": [
{
"name": "List Customers",
"request": {
"method": "GET",
"url": "{{base_url}}{{api_path}}/customers?limit=10"
}
},
{
"name": "Create Customer",
"request": {
"method": "POST",
"url": "{{base_url}}{{api_path}}/customers",
"body": {
"mode": "raw",
"raw": "{ \"name\": \"Test Customer\", \"email\": \"[email protected]\" }"
}
}
}
]
}
Postman テスト:
// Test response status
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
// Test response structure
pm.test("Response has result array", function () {
var jsonData = pm.response.json();
pm.expect(jsonData).to.have.property('result');
pm.expect(jsonData.result).to.be.an('array');
});
// Test pagination
pm.test("Response has meta with pagination", function () {
var jsonData = pm.response.json();
pm.expect(jsonData.meta).to.have.property('total');
pm.expect(jsonData.meta).to.have.property('limit');
pm.expect(jsonData.meta).to.have.property('offset');
});
// Save sys_id for subsequent requests
pm.test("Save created customer ID", function () {
var jsonData = pm.response.json();
if (jsonData.result && jsonData.result.sys_id) {
pm.environment.set("customer_id", jsonData.result.sys_id);
}
});
フェーズ 9:公開と有効化
ステップ 9.1:公開前のチェックリスト
公開する前に、次の項目を確認します。
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_operation
query: web_service_definition=[api_definition_sys_id]
fields: name,http_method,relative_path,requires_authentication
チェックリスト:
- すべてのオペレーションで認証が有効になっている
- すべてのスクリプトにエラー処理が実装されている
- レート制限が設定されている
- 入力検証が完了している
- ACL の強制が有効になっている
- ドキュメントが更新されている
- テストケースがすべて成功している
- 更新セットが取得されている
ステップ 9.2:API を公開する
Tool: SN-Update-Record
Parameters:
table_name: sys_ws_definition
sys_id: [api_definition_sys_id]
data:
published: true
active: true
ステップ 9.3:公開を確認する
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_definition
query: sys_id=[api_definition_sys_id]
fields: name,active,published,service_address
API は service_address URL でアクセスできるようになりました。
ツール使用の概要
| オペレーション | MCP ツール | 目的 |
|---|---|---|
| API の作成 | SN-Create-Record (sys_ws_definition) | API コンテナーを作成する |
| リソースの作成 | SN-Create-Record (sys_ws_operation) | エンドポイントを追加する |
| API の更新 | SN-Update-Record | 設定を変更する |
| API のクエリ | SN-Query-Table | API を一覧表示/確認する |
| スキーマの検出 | SN-Get-Table-Schema | テーブル構造を把握する |
| スクリプトのテスト | SN-Execute-Background-Script | API ロジックをテストする |
ベストプラクティス
- 設計を優先: 実装前に API 構造を計画します
- 最初からバージョン管理: 常にバージョン管理を有効にします(is_versioned: true)
- 一貫したレスポンス: 標準的なエラー/成功フォーマットを使用します
- すべてを検証: 処理前に入力を検証します
- デフォルトでセキュアに: すべてのエンドポイントで認証を有効にします
- 十分にドキュメント化: short_description と doc_link を使用します
- 徹底的にテスト: 公開前に curl/Postman を使用します
- 使用状況を監視: デバッグとメトリクスのために API 呼び出しをログに記録します
- レート制限: 悪用を防ぎます
- スクリプトインクルードを使用: 再利用可能なロジックを一元管理します
トラブルシューティング
API が 404 を返す
症状: API エンドポイントが見つかりません 原因:
- API が公開されていない/有効になっていない
- URL パスが正しくない
- 名前空間が正しくない 解決策:
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_definition
query: name=[your_api_name]
fields: active,published,service_address,namespace
401 Unauthorized
症状: 認証に失敗します 原因:
- 認証情報が無効
- ユーザーに REST API へのアクセス権がない
- web_service_admin ロールがない 解決策:
- まず Table API で認証情報を確認します
- ユーザーに rest_service ロールがあることを確認します
- カスタム認証を使用している場合は、API キーを確認します
403 Forbidden
症状: 認証後にアクセスが拒否されます 原因:
- ACL によってアクセスがブロックされている
- テーブル権限がない
- スコープ付きアプリの制限 解決策:
- 基盤となるテーブルの ACL を確認します
- ユーザーロールが ACL の要件を満たしていることを確認します
- API の enforce_acl 設定を確認します
500 Internal Server Error
症状: 詳細のないサーバーエラー 原因:
- オペレーション内のスクリプトエラー
- 無効な GlideRecord クエリ
- スクリプト内の null 参照 解決策:
- System Logs でスクリプトエラーを確認します
- オペレーションスクリプトに try-catch を追加します
- まずバックグラウンドスクリプトでスクリプトをテストします
レスポンスボディが空
症状: ステータスは 200 ですが、ボディがありません 原因:
- response.setBody() がない
- スクリプトが早期に終了した
- return の前にボディが設定されている 解決策:
- response.setBody() が呼び出されていることを確認します
- すべてのコードパスでレスポンスが設定されることを確認します
- レスポンスを返さずに早期 return する箇所がないことを確認します
スクリプトが実行されない
症状: API はレスポンスを返しますが、スクリプトが実行されません 原因:
- スクリプトの構文エラー
- オペレーションが API にリンクされていない
- HTTP メソッドが正しくない 解決策:
Tool: SN-Query-Table
Parameters:
table_name: sys_ws_operation
query: web_service_definition=[api_sys_id]
fields: name,http_method,active,operation_script
例
例 1:最小限の CRUD API
基本的な CRUD を備えた最小限の API 一式:
# 1. Create API Definition
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_definition
data:
name: Simple Task API
namespace: x_company
short_description: Simple task management API
active: true
is_versioned: true
baseline_version: v1
# 2. Create List Operation
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: List Tasks
web_service_definition: [api_sys_id]
http_method: GET
relative_path: /tasks
operation_script: |
(function process(request, response) {
var gr = new GlideRecord('x_company_task');
gr.setLimit(100);
gr.query();
var tasks = [];
while (gr.next()) {
tasks.push({
sys_id: gr.getUniqueValue(),
name: gr.getValue('name'),
status: gr.getValue('status')
});
}
response.setBody({ result: tasks });
})(request, response);
# 3. Create Get Operation
Tool: SN-Create-Record
Parameters:
table_name: sys_ws_operation
data:
name: Get Task
web_service_definition: [api_sys_id]
http_method: GET
relative_path: /tasks/{id}
operation_script: |
(function process(request, response) {
var gr = new GlideRecord('x_company_task');
if (gr.get(request.pathParams.id)) {
response.setBody({
result: {
sys_id: gr.getUniqueValue(),
name: gr.getValue('name'),
description: gr.getValue('description'),
status: gr.getValue('status')
}
});
} else {
response.setStatus(404);
response.setBody({ error: { message: 'Task not found' }});
}
})(request, response);
例 2:集計を使用した検索 API
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var searchTerm = request.queryParams.q;
var category = request.queryParams.category;
var includeStats = request.queryParams.stats === 'true';
// Search incidents
var gr = new GlideRecord('incident');
if (searchTerm) {
var qc = gr.addQuery('short_description', 'CONTAINS', searchTerm);
qc.addOrCondition('description', 'CONTAINS', searchTerm);
qc.addOrCondition('number', searchTerm);
}
if (category) {
gr.addQuery('category', category);
}
gr.addQuery('active', true);
gr.setLimit(50);
gr.orderByDesc('sys_created_on');
gr.query();
var results = [];
while (gr.next()) {
results.push({
number: gr.getValue('number'),
short_description: gr.getValue('short_description'),
priority: gr.getDisplayValue('priority'),
state: gr.getDisplayValue('state'),
created: gr.getValue('sys_created_on')
});
}
var responseBody = {
query: searchTerm,
count: results.length,
results: results
};
// Add statistics if requested
if (includeStats) {
var stats = {};
// Count by priority
var priorityAgg = new GlideAggregate('incident');
priorityAgg.addQuery('active', true);
priorityAgg.addAggregate('COUNT');
priorityAgg.groupBy('priority');
priorityAgg.query();
stats.by_priority = {};
while (priorityAgg.next()) {
var priority = priorityAgg.getDisplayValue('priority');
stats.by_priority[priority] = parseInt(priorityAgg.getAggregate('COUNT'));
}
responseBody.stats = stats;
}
response.setStatus(200);
response.setBody(responseBody);
})(request, response);
例 3:一括操作 API
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var body = request.body.data;
// Validate bulk request
if (!body.operations || !Array.isArray(body.operations)) {
response.setStatus(400);
response.setBody({
error: {
message: 'operations array required',
code: 'VALIDATION_ERROR'
}
});
return;
}
if (body.operations.length > 100) {
response.setStatus(400);
response.setBody({
error: {
message: 'Maximum 100 operations per request',
code: 'LIMIT_EXCEEDED'
}
});
return;
}
var results = [];
body.operations.forEach(function(op, index) {
var result = { index: index, operation: op.action };
try {
switch (op.action) {
case 'create':
var gr = new GlideRecord('customer');
gr.initialize();
for (var field in op.data) {
gr.setValue(field, op.data[field]);
}
result.sys_id = gr.insert();
result.success = !!result.sys_id;
break;
case 'update':
var grUpdate = new GlideRecord('customer');
if (grUpdate.get(op.sys_id)) {
for (var updateField in op.data) {
grUpdate.setValue(updateField, op.data[updateField]);
}
grUpdate.update();
result.success = true;
} else {
result.success = false;
result.error = 'Record not found';
}
break;
case 'delete':
var grDelete = new GlideRecord('customer');
if (grDelete.get(op.sys_id)) {
grDelete.setValue('active', false);
grDelete.update();
result.success = true;
} else {
result.success = false;
result.error = 'Record not found';
}
break;
default:
result.success = false;
result.error = 'Unknown action: ' + op.action;
}
} catch (e) {
result.success = false;
result.error = e.message;
}
results.push(result);
});
var successCount = results.filter(function(r) { return r.success; }).length;
response.setStatus(successCount === results.length ? 200 : 207); // 207 = Multi-Status
response.setBody({
summary: {
total: results.length,
succeeded: successCount,
failed: results.length - successCount
},
results: results
});
})(request, response);
関連スキル
admin/update-set-management- 更新セットに API 定義を取り込むadmin/application-scope- スコープ付きアプリケーションで API を作成するsecurity/acl-management- API セキュリティを設定するadmin/script-execution- API スクリプトをテストするcatalog/request-fulfillment- API 駆動のリクエスト処理