This document provides comprehensive guidance for interacting with the iTop REST/JSON API, including endpoint patterns, OQL query construction, error handling, and integration best practices.
https://{itop-server}/webservices/rest.php?version={api-version}
Example:
http://192.168.139.92/webservices/rest.php?version=1.3
API Version: This integration targets version 1.3 (iTop 3.1+)
All requests MUST include authentication via the Auth-Token header:
POST /webservices/rest.php?version=1.3 HTTP/1.1
Host: itop-server.example.com
Content-Type: application/x-www-form-urlencoded
Auth-Token: <application-token-here>
User-Agent: Nextcloud-iTop-Integration/1.0
json_data={"operation":"core/get",...}Implementation: ItopAPIService.php:614-680
$options = [
'headers' => [
'Auth-Token' => $this->getApplicationToken(),
'User-Agent' => 'Nextcloud iTop integration',
],
'form_params' => [
'json_data' => json_encode($params)
]
];
$response = $this->client->post($url, $options);IMPORTANT: Use POST with form_params (not JSON body or GET with URL encoding).
β This FAILS for SELECT queries:
// BAD - URL encoding breaks OQL SELECT queries
$url = $apiUrl . '&json_data=' . urlencode(json_encode($params));
$response = $this->client->get($url);β This WORKS reliably:
// GOOD - POST with form_params
$options = [
'form_params' => [
'json_data' => json_encode($params)
]
];
$response = $this->client->post($url, $options);Why? iTop's REST API parser expects POST requests with application/x-www-form-urlencoded for complex OQL queries.
All API operations are encoded in the json_data parameter:
{
"operation": "core/get",
"class": "PC",
"key": "SELECT PC WHERE name LIKE '%laptop%'",
"output_fields": "id,name,status,org_id_friendlyname"
}Purpose: List all available API operations (useful for token validation)
Request:
{
"operation": "list_operations"
}Response:
{
"code": 0,
"message": "Success",
"version": "1.3",
"operations": [
"core/check_credentials",
"core/get",
"core/create",
"core/update",
"core/delete",
"core/get_related",
"core/apply_stimulus"
]
}Use Cases:
- Validate application token connectivity
- Check API version compatibility
Implementation: ConfigController.php:278-412
Purpose: Validate authentication credentials
Request:
{
"operation": "core/check_credentials"
}Response (Success):
{
"code": 0,
"message": "User authenticated successfully",
"version": "1.3",
"user": {
"login": "admin",
"first_name": "Tester",
"last_name": "Admin",
"email": "admin@example.com",
"org_name": "Demo"
},
"privileges": ["Administrator", "Configuration Manager"]
}Response (Failure):
{
"code": 1,
"message": "Error: Invalid token or Portal user is not allowed"
}Use Cases:
- Admin settings: Test application token
- Personal settings: Validate personal token (before Phase 2 migration)
Portal User Limitation: This operation returns error code 1 for Portal users even with valid personal tokens (see security-auth.md)
Purpose: Retrieve objects from iTop CMDB
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
operation |
string | Yes | Must be "core/get" |
class |
string | Yes | iTop class name (e.g., "PC", "Person", "UserRequest") |
key |
string/int | Yes | Numeric ID, OQL query, or "SELECT ..." |
output_fields |
string | No | Comma-separated field list (default: all fields) |
Request:
{
"operation": "core/get",
"class": "Person",
"key": 3,
"output_fields": "id,first_name,name,email,org_id_friendlyname"
}Response:
{
"code": 0,
"message": "Success",
"objects": {
"Person::3": {
"code": 0,
"message": "",
"class": "Person",
"key": "3",
"fields": {
"id": "3",
"first_name": "Boris",
"name": "Bereznay",
"email": "boris@example.com",
"org_id_friendlyname": "Demo"
}
}
}
}Request:
{
"operation": "core/get",
"class": "PC",
"key": "SELECT PC WHERE org_id_friendlyname = 'Demo' AND status = 'production'",
"output_fields": "id,name,status,brand_id_friendlyname,model_id_friendlyname"
}Response:
{
"code": 0,
"message": "Success",
"objects": {
"PC::1": {
"code": 0,
"message": "",
"class": "PC",
"key": "1",
"fields": {
"id": "1",
"name": "LAPTOP-001",
"status": "production",
"brand_id_friendlyname": "Dell",
"model_id_friendlyname": "Latitude 7420"
}
},
"PC::5": {
"code": 0,
"message": "",
"class": "PC",
"key": "5",
"fields": {
"id": "5",
"name": "DESKTOP-042",
"status": "production",
"brand_id_friendlyname": "HP",
"model_id_friendlyname": "EliteDesk 800 G6"
}
}
}
}Special Feature: iTop provides a magic placeholder :current_contact_id that resolves to the authenticated user's Person ID.
Request:
{
"operation": "core/get",
"class": "Person",
"key": "SELECT Person WHERE id = :current_contact_id",
"output_fields": "id,first_name,name,email"
}Use Cases:
- Personal token validation (extracts Person ID without prior knowledge)
- Self-service operations (user updating own information)
Implementation: ConfigController.php:645-750
Purpose: Create new objects in iTop
Status: β NOT USED in this integration (read-only by design)
Purpose: Update existing objects in iTop
Status: β NOT USED in this integration (read-only by design)
Purpose: Delete objects from iTop
Status: β NOT USED in this integration (read-only by design)
Object Query Language (OQL) is iTop's SQL-like query language.
SELECT <class>
[WHERE <conditions>]
[JOIN <class> ON <field> = <field>]
[LIMIT <number>]Examples:
-- All PCs
SELECT PC
-- PCs in production status
SELECT PC WHERE status = 'production'
-- PCs with specific name pattern
SELECT PC WHERE name LIKE '%laptop%'
-- PCs for a specific organization
SELECT PC WHERE org_id_friendlyname = 'Demo'
-- Limit results
SELECT PC LIMIT 10SELECT PC WHERE name = 'LAPTOP-001'
SELECT PC WHERE id = 5
SELECT PC WHERE status = 'production'iTop uses _id suffix for foreign keys and _id_friendlyname for readable names.
-- Filter by organization ID
SELECT PC WHERE org_id = 1
-- Filter by organization name (recommended)
SELECT PC WHERE org_id_friendlyname = 'Demo'
-- Filter by brand name
SELECT PC WHERE brand_id_friendlyname = 'Dell'Why _friendlyname?
- Easier for admins to read/write
- Stable across environments (IDs may differ)
- Natural language filtering
| Operator | Usage | Example |
|---|---|---|
= |
Exact match | status = 'production' |
!= |
Not equal | status != 'obsolete' |
LIKE |
Pattern match (% wildcard) | name LIKE '%laptop%' |
>, <, >=, <= |
Numeric/date comparison | move2production > '2024-01-01' |
IN |
List membership | status IN ('active', 'production') |
MATCHES |
Join to related class | See below |
Purpose: Query through external keys and relationships
-- PCs where the organization's code is 'DEMO'
SELECT PC WHERE org_id MATCHES Organization WHERE code = 'DEMO'Critical for Portal Users: Filter CIs by contact relationship
-- PCs where Person ID 3 is listed as a contact
SELECT PC WHERE contacts_list MATCHES Person WHERE id = 3Breakdown:
contacts_list: N-N relationship field on PC (links tolnkContactToFunctionalCI)MATCHES Person WHERE id = 3: Filters to PCs related to Person #3
Implementation Pattern:
// Portal-only users
$personId = $this->getPersonId($userId);
$query = "SELECT PC WHERE contacts_list MATCHES Person WHERE id = $personId";
// Returns only PCs where the user is listed as a contactRelated Classes:
| CI Class | Contact Relationship Field |
|---|---|
PC |
contacts_list |
Phone |
contacts_list |
Printer |
contacts_list |
WebApplication |
contacts_list |
All FunctionalCI |
contacts_list |
SELECT PC
WHERE org_id_friendlyname = 'Demo'
AND status = 'production'
AND name LIKE '%laptop%'SELECT FunctionalCI
WHERE finalclass = 'PC'
OR finalclass = 'Phone'
OR finalclass = 'Tablet'Note: Use parentheses for precedence:
SELECT PC
WHERE (status = 'production' OR status = 'implementation')
AND org_id_friendlyname = 'Demo'CRITICAL: Always escape user input in OQL queries to prevent injection
// Escape single quotes
$escapedTerm = str_replace("'", "\\'", $userInput);
$query = "SELECT PC WHERE name LIKE '%$escapedTerm%'";Example:
// User input: "Alice's Laptop"
$userInput = "Alice's Laptop";
$escapedTerm = str_replace("'", "\\'", $userInput); // "Alice\\'s Laptop"
$query = "SELECT PC WHERE name LIKE '%$escapedTerm%'"; // SafeImplementation: ItopAPIService.php:422
Use Case: Search across multiple CI classes simultaneously
-- All devices (PCs, Phones, Tablets)
SELECT FunctionalCI
WHERE finalclass IN ('PC', 'Phone', 'Tablet')
AND status = 'production'Implementation Strategy for CI Browsing:
// Search across all enabled CI classes
$enabledClasses = ['PC', 'Phone', 'Tablet', 'Printer', 'PCSoftware', 'WebApplication'];
$classFilter = "finalclass IN ('" . implode("','", $enabledClasses) . "')";
$query = "SELECT FunctionalCI WHERE $classFilter AND name LIKE '%$term%'";Performance Note: Querying FunctionalCI with finalclass filter is more efficient than separate queries per class.
Recommendation: Always specify output_fields to minimize payload size
{
"operation": "core/get",
"class": "PC",
"key": "SELECT PC LIMIT 10",
"output_fields": "id,name,status,org_id_friendlyname"
}Wildcard: Use * for all fields (not recommended for production)
{
"output_fields": "*"
}Defined in class-mapping.md
id,name,finalclass,org_id_friendlyname,status,asset_number,serialnumber,last_update
Purpose: Fast search results with minimal data
id,name,finalclass,org_id_friendlyname,status,business_criticity,location_id_friendlyname,
move2production,asset_number,serialnumber,brand_id_friendlyname,model_id_friendlyname,
last_update,description
Purpose: Full CI preview widget rendering
PC:
,type,osfamily_id_friendlyname,osversion_id_friendlyname,cpu,ram
Phone/IPPhone:
,phonenumber
MobilePhone:
,phonenumber,imei
WebApplication:
,url,webserver_name
PCSoftware/OtherSoftware:
,system_name,software_id_friendlyname,softwarelicence_id_friendlyname,path
Pattern: For every <field>_id, there's a <field>_id_friendlyname
| Field ID | Friendly Name | Example Value |
|---|---|---|
org_id |
org_id_friendlyname |
"Demo" |
brand_id |
brand_id_friendlyname |
"Dell" |
model_id |
model_id_friendlyname |
"Latitude 7420" |
location_id |
location_id_friendlyname |
"Vienna Office - Floor 3" |
osfamily_id |
osfamily_id_friendlyname |
"Windows" |
Recommendation: Always use _friendlyname for display purposes
{
"code": 0,
"message": "Success",
"objects": {
"<Class>::<ID>": {
"code": 0,
"message": "",
"class": "<Class>",
"key": "<ID>",
"fields": {
"id": "<ID>",
"field1": "value1",
"field2": "value2"
}
}
}
}{
"code": 0,
"message": "Success",
"objects": {}
}Check: isset($response['objects']) && !empty($response['objects'])
| Code | Meaning | Example Message |
|---|---|---|
0 |
Success | "Success" |
1 |
Authentication error | "Invalid token" or "Portal user is not allowed" |
2 |
Missing parameter | "Missing parameter: class" |
3 |
Invalid class | "Unknown class: InvalidClass" |
4 |
Invalid key/OQL | "Invalid OQL query" |
100 |
Internal error | "Internal error" |
Implementation:
if (!isset($result['code']) || $result['code'] !== 0) {
$error = $result['message'] ?? 'Unknown error';
return ['error' => $error, 'error_code' => $result['code'] ?? null];
}OQL Syntax:
SELECT PC LIMIT 20Recommendation: Use limits to prevent large payloads
// Unified Search: 20 results globally (distributed across classes)
// Per-class limit: ceil(20 / count($enabledClasses))
$perClassLimit = 5;
$query = "SELECT PC WHERE name LIKE '%$term%' LIMIT $perClassLimit";Limitation: iTop OQL does not support OFFSET or SKIP
Workaround for Pagination:
// Fetch more than needed, slice in PHP
$query = "SELECT PC LIMIT 100";
$results = $this->request($userId, $params);
// Client-side pagination
$page = 2;
$perPage = 10;
$offset = ($page - 1) * $perPage;
$pageResults = array_slice($results['objects'], $offset, $perPage);Performance Note: For large datasets, use filtering instead of pagination:
- Filter by status, organization, or date range
- Use search terms to narrow results
Use Case: Personal token validation, user info display
{
"operation": "core/get",
"class": "Person",
"key": "SELECT Person WHERE id = :current_contact_id",
"output_fields": "id,first_name,name,email,org_id_friendlyname"
}Use Case: Portal user CI browsing
{
"operation": "core/get",
"class": "FunctionalCI",
"key": "SELECT FunctionalCI WHERE contacts_list MATCHES Person WHERE id = 3",
"output_fields": "id,name,finalclass,status,org_id_friendlyname"
}Use Case: Unified search for power users
{
"operation": "core/get",
"class": "FunctionalCI",
"key": "SELECT FunctionalCI WHERE finalclass IN ('PC','Phone','Tablet','Printer') AND name LIKE '%laptop%'",
"output_fields": "id,name,finalclass,status,org_id_friendlyname"
}Use Case: Rich preview from pasted iTop URL
URL Pattern:
http://itop-server/pages/UI.php?operation=details&class=PC&id=5
Query:
{
"operation": "core/get",
"class": "PC",
"key": 5,
"output_fields": "id,name,status,org_id_friendlyname,brand_id_friendlyname,model_id_friendlyname,cpu,ram"
}Use Case: Dashboard widget, ticket search
// Step 1: Get user's full name
$userInfo = $this->getCurrentUser($userId);
$fullName = $userInfo['user']['first_name'] . ' ' . $userInfo['user']['last_name'];
// Step 2: Query UserRequests
$query1 = "SELECT UserRequest WHERE caller_id_friendlyname = '$fullName' AND status != 'closed'";
// Step 3: Query Incidents
$query2 = "SELECT Incident WHERE caller_id_friendlyname = '$fullName' AND status != 'closed'";Implementation: ItopAPIService.php:176-251
try {
$response = $this->client->post($url, $options);
} catch (ConnectException $e) {
// Network failure (DNS, timeout, connection refused)
return [
'error' => 'Connection failed: ' . $e->getMessage(),
'error_type' => 'network'
];
} catch (ServerException $e) {
// HTTP 5xx errors
$statusCode = $e->getResponse()->getStatusCode();
return [
'error' => 'iTop server error',
'error_code' => $statusCode,
'error_type' => 'server'
];
} catch (ClientException $e) {
// HTTP 4xx errors
$statusCode = $e->getResponse()->getStatusCode();
if ($statusCode === 401) {
return [
'error' => 'Authentication failed - invalid token',
'error_code' => 401,
'error_type' => 'auth'
];
}
return [
'error' => 'Client error: ' . $statusCode,
'error_code' => $statusCode,
'error_type' => 'client'
];
}Implementation: ItopAPIService.php:665-679
$result = json_decode($response->getBody(), true);
// Check JSON decode
if ($result === null) {
return ['error' => 'Invalid JSON response', 'error_type' => 'parse'];
}
// Check API error code
if (!isset($result['code']) || $result['code'] !== 0) {
$errorMsg = $result['message'] ?? 'Unknown error';
// Special handling for Portal users
if (strpos($errorMsg, 'Portal user is not allowed') !== false) {
return [
'error' => 'Portal users must use application token flow',
'error_type' => 'portal_restriction',
'hint' => 'This is expected - configure personal token for identity verification only'
];
}
return [
'error' => $errorMsg,
'error_code' => $result['code'],
'error_type' => 'api'
];
}
// Check for empty results
if (!isset($result['objects']) || empty($result['objects'])) {
return ['error' => 'No results found', 'error_type' => 'empty'];
}if (isset($apiResult['error'])) {
// Log server-side (without sensitive data)
$this->logger->warning('iTop API error', [
'error_type' => $apiResult['error_type'] ?? 'unknown',
'user' => $userId
]);
// Return user-friendly message
$userMessage = match($apiResult['error_type'] ?? '') {
'network' => $this->l10n->t('Could not connect to iTop server'),
'auth' => $this->l10n->t('Authentication failed - check your settings'),
'portal_restriction' => $this->l10n->t('Portal user restriction - contact administrator'),
'empty' => $this->l10n->t('No results found'),
default => $this->l10n->t('An error occurred: ') . $apiResult['error']
};
return new DataResponse(['error' => $userMessage], Http::STATUS_BAD_REQUEST);
}β Bad:
{"operation": "core/get", "class": "PC", "key": "SELECT PC", "output_fields": "*"}β Good:
{"operation": "core/get", "class": "PC", "key": "SELECT PC", "output_fields": "id,name,status"}Impact: 80% payload reduction for large objects
β Bad:
SELECT FunctionalCI
-- Returns thousands of objectsβ Good:
SELECT FunctionalCI WHERE org_id_friendlyname = 'Demo' AND status = 'production' LIMIT 20Impact: 100x faster for large CMDBs
β Bad:
// 3 separate API calls
$pcs = $this->request(['key' => 'SELECT PC LIMIT 5']);
$phones = $this->request(['key' => 'SELECT Phone LIMIT 5']);
$tablets = $this->request(['key' => 'SELECT Tablet LIMIT 5']);β Good:
// 1 API call
$all = $this->request([
'class' => 'FunctionalCI',
'key' => "SELECT FunctionalCI WHERE finalclass IN ('PC','Phone','Tablet') LIMIT 15"
]);Impact: 3x reduction in API calls
See caching-performance.md for detailed caching strategies
Quick Example:
$cacheKey = 'ci_preview_' . $userId . '_' . $class . '_' . $id;
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return json_decode($cached, true);
}
$result = $this->request($userId, $params);
$this->cache->set($cacheKey, json_encode($result), 60); // 60s TTL
return $result;// Escape OQL parameters
$safeTerm = str_replace("'", "\\'", $userInput);
$query = "SELECT PC WHERE name LIKE '%$safeTerm%'";// NEVER accept person_id from client
public function search(string $userId, string $term): array {
// GOOD - retrieve person_id server-side
$personId = $this->getPersonId($userId);
if (!$personId) {
throw new Exception('User not configured');
}
// Use in query
$query = "SELECT PC WHERE contacts_list MATCHES Person WHERE id = $personId";
}// Check user profile before allowing full CMDB access
$isPortalOnly = $this->profileService->isPortalOnly($userId);
if ($isPortalOnly) {
// Restrict to contact-related CIs
$query = "SELECT PC WHERE contacts_list MATCHES Person WHERE id = $personId";
} else {
// Allow full search within ACL
$query = "SELECT PC WHERE name LIKE '%$term%'";
}// BAD
$this->logger->info('Query: ' . $query); // May contain person names
// GOOD
$this->logger->info('CI search executed', [
'user' => $userId,
'class' => $class,
'result_count' => count($results)
]);Recommendation: 5 requests/second/user for interactive features
Implementation: See caching-performance.md
Test 1: Validate Application Token
curl -s --location -g --request POST \
'http://192.168.139.92/webservices/rest.php?version=1.3' \
--header 'Auth-Token: YOUR_APP_TOKEN_HERE' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'json_data={"operation":"list_operations"}' | jqExpected:
{
"code": 0,
"message": "Success",
"operations": [...]
}Test 2: Get Person by :current_contact_id
curl -s --location -g --request POST \
'http://192.168.139.92/webservices/rest.php?version=1.3' \
--header 'Auth-Token: YOUR_PERSONAL_TOKEN_HERE' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'json_data={"operation":"core/get","class":"Person","key":"SELECT Person WHERE id = :current_contact_id","output_fields":"id,first_name,name,email"}' | jqExpected:
{
"code": 0,
"objects": {
"Person::3": {
"fields": {
"id": "3",
"first_name": "Boris",
"name": "Bereznay",
"email": "boris@example.com"
}
}
}
}Test 3: Get Contact CIs
curl -s --location -g --request POST \
'http://192.168.139.92/webservices/rest.php?version=1.3' \
--header 'Auth-Token: YOUR_APP_TOKEN_HERE' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'json_data={"operation":"core/get","class":"FunctionalCI","key":"SELECT FunctionalCI WHERE contacts_list MATCHES Person WHERE id = 3","output_fields":"id,name,finalclass,status"}' | jqEnable verbose logging:
// In ItopAPIService::request()
$this->logger->debug('iTop API Request', [
'operation' => $params['operation'] ?? 'unknown',
'class' => $params['class'] ?? 'unknown',
'user' => $userId
]);
$this->logger->debug('iTop API Response', [
'code' => $result['code'] ?? 'missing',
'object_count' => isset($result['objects']) ? count($result['objects']) : 0
]);Log Location: nextcloud/data/nextcloud.log
Symptom:
{"code": 1, "message": "Error: Portal user is not allowed"}Cause: Portal users cannot use REST API directly (iTop core restriction)
Solution: Use dual-token architecture (see security-auth.md)
- Personal token for identity verification only
- Application token for all queries
Symptom:
{"code": 0, "objects": {}}Debug Steps:
- Verify OQL syntax in iTop console (Data Administration β OQL Queries)
- Check field names match iTop data model
- Verify user has permissions to query the class
- Test with simpler query (e.g.,
SELECT PC LIMIT 1)
Common Mistake: Using org_id instead of org_id_friendlyname
Symptom: HTTP 400 or malformed query error
Cause: URL encoding breaks complex OQL
Solution: Use POST with form_params (not GET)
Symptom:
{"code": 0, "objects": {}}Cause: User doesn't have a linked Person record in iTop
Solution: Admin must create Person record and link to User account (contactid field)
| iTop Version | API Version | Compatibility | Notes |
|---|---|---|---|
| 3.1.x | 1.3 | β Full | Target version |
| 3.0.x | 1.3 | β Full | Compatible |
| 2.7.x | 1.3 | Test thoroughly | |
| 2.6.x and older | 1.2 | β Not tested | May need changes |
Recommendation: Require iTop 3.0+ in documentation
- Batch queries - Multiple operations in one request (iTop 3.2+)
- GraphQL support - If iTop adds GraphQL endpoint
- Webhook subscriptions - Real-time CI updates
- Field metadata caching - Reduce data model queries
- Official API Docs: https://www.itophub.io/wiki/page?id=latest:advancedtopics:rest_json
- OQL Reference: https://www.itophub.io/wiki/page?id=latest:oql:start
- Implementation: ItopAPIService.php
- Security: security-auth.md
- Class Mapping: class-mapping.md