Mastering the MAS REST API: A Practitioner's Guide
Series: MAS INTEGRATION -- From Legacy MIF to Cloud-Native Integration | Part 4 of 8
Read Time: 25 minutes
Who this is for: Integration developers, backend engineers, and automation specialists who need to build, query, and manage Maximo data programmatically through the REST API. This is a hands-on reference -- code first, theory second.
The bottom line: This is the post you will bookmark. Every authentication method, every CRUD operation, every query pattern, every error code -- documented with working code examples in curl, Python, and Node.js. When you are building integrations at 2 AM and need the exact syntax, this is where you come back.
The API You Will Actually Use
Parts 2 and 3 of this series gave you the conceptual foundation: API-first architecture, event-driven patterns, the paradigm shift from MIF. This post is different. This post is the hands-on reference. You are going to see real HTTP requests, real response bodies, real error messages, and real code that handles all of it.
Every example in this post targets the OSLC-based REST API that ships with MAS Manage. The base URL pattern you will use throughout is:
https://{mas-host}/maximo/oslc/os/{objectstructure}Common object structures you will work with daily:
Object Structure — Resource — Description
mxwo — Work Orders — Work order management
mxasset — Assets — Asset records
mxsr — Service Requests — Service request management
mxpo — Purchase Orders — Procurement
mxinventory — Inventory — Inventory balances
mxperson — Persons — Person records
mxitem — Items — Item master
mxloc — Locations — Location hierarchy
Let us start where every integration starts: getting authenticated.
Authentication Deep Dive
MAS supports three authentication methods. Each serves a different use case. Pick the right one and your integration life gets dramatically simpler.
Method 1: API Key Authentication
API keys are the simplest authentication method and the right choice for server-to-server integrations, batch jobs, and automation scripts. You generate them in MAS Administration and pass them as a header on every request.
Generating an API Key:
- Log into MAS Administration
- Navigate to Administration > API Keys
- Click Add API Key
- Associate the key with a user account (this determines permissions)
- Copy the key -- you will not see it again
curl:
# Simple authenticated request with API key
curl -X GET "https://mas-host/maximo/oslc/os/mxwo?oslc.pageSize=1" \
-H "apikey: a1b2c3d4e5f6g7h8i9j0" \
-H "Accept: application/json"Python:
import requests
BASE_URL = "https://mas-host/maximo/oslc/os"
API_KEY = "a1b2c3d4e5f6g7h8i9j0"
headers = {
"apikey": API_KEY,
"Accept": "application/json"
}
response = requests.get(f"{BASE_URL}/mxwo?oslc.pageSize=1", headers=headers)
print(response.status_code)
print(response.json())Node.js:
const axios = require('axios');
const BASE_URL = 'https://mas-host/maximo/oslc/os';
const API_KEY = 'a1b2c3d4e5f6g7h8i9j0';
async function getWorkOrder() {
const response = await axios.get(`${BASE_URL}/mxwo`, {
params: { 'oslc.pageSize': 1 },
headers: {
'apikey': API_KEY,
'Accept': 'application/json'
}
});
console.log(response.data);
}
getWorkOrder();When to use API keys: Scheduled jobs, middleware integrations, scripts that run under a service account. The key inherits permissions from its associated user, so create a dedicated service account with exactly the permissions your integration needs -- no more, no less.
Method 2: OAuth 2.0 Client Credentials
OAuth 2.0 is the recommended method when you need user-context operations or when your organization's security policy requires token-based authentication with expiration. MAS uses the OpenID Connect (OIDC) protocol backed by an identity provider -- typically IBM Cloud Pak foundational services or an external IdP like Azure AD.
Python:
import requests
# Step 1: Get OAuth token
token_url = "https://mas-host/auth/realms/mas/protocol/openid-connect/token"
token_response = requests.post(token_url, data={
'grant_type': 'client_credentials',
'client_id': 'my-integration-app',
'client_secret': 'your-client-secret'
})
access_token = token_response.json()['access_token']
# Step 2: Use token in API calls
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
response = requests.get(
"https://mas-host/maximo/oslc/os/mxwo?oslc.pageSize=10",
headers=headers
)
print(response.json())Node.js:
const axios = require('axios');
async function getOAuthToken() {
const tokenUrl = 'https://mas-host/auth/realms/mas/protocol/openid-connect/token';
const response = await axios.post(tokenUrl, new URLSearchParams({
grant_type: 'client_credentials',
client_id: 'my-integration-app',
client_secret: 'your-client-secret'
}), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
return response.data.access_token;
}
async function queryWorkOrders() {
const token = await getOAuthToken();
const response = await axios.get('https://mas-host/maximo/oslc/os/mxwo', {
params: { 'oslc.pageSize': 10 },
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
console.log(response.data);
}
queryWorkOrders();Token management tip: OAuth tokens expire. In production, cache the token and refresh it before expiration. A typical access token lifetime is 300 seconds (5 minutes). Do not request a new token on every API call -- that is wasteful and may trigger rate limits on the identity provider.
Method 3: OIDC Discovery with openid-client
For Node.js applications that need the full OIDC flow -- including automatic discovery of endpoints and token refresh -- use the openid-client library. This is the most robust approach for long-running Node.js services.
const { Issuer } = require('openid-client');
async function getMaximoClient() {
// Discover OIDC endpoints automatically
const issuer = await Issuer.discover('https://mas-host/auth/realms/mas');
console.log('Discovered issuer:', issuer.metadata.issuer);
// Create client
const client = new issuer.Client({
client_id: 'my-integration-app',
client_secret: 'your-client-secret'
});
// Get token using client credentials grant
const tokenSet = await client.grant({ grant_type: 'client_credentials' });
console.log('Token expires at:', tokenSet.expires_at);
return tokenSet.access_token;
}
async function fetchAssets() {
const token = await getMaximoClient();
const response = await fetch('https://mas-host/maximo/oslc/os/mxasset?oslc.pageSize=5', {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
const data = await response.json();
console.log(`Found ${data.member.length} assets`);
return data;
}
fetchAssets();When to use OIDC discovery: Production Node.js services that need automatic endpoint resolution, token refresh, and standards-compliant OIDC flows. The discovery mechanism means you do not hardcode token endpoints -- if the IdP configuration changes, your code adapts automatically.
Authentication Method Comparison
Method — Best For — Complexity — Token Expiry — User Context
API Key — Server-to-server, scripts, batch jobs — Low — None (rotate manually) — Service account
OAuth 2.0 Client Credentials — Application-to-application — Medium — 5-15 minutes — Service account
OIDC with openid-client — Long-running Node.js services — Higher — Auto-refresh capable — Service or user
CRUD Operations
Every integration boils down to four operations: Create, Read, Update, Delete. Here is how each one works in the MAS REST API, with complete examples.
CREATE -- POST
Creating a record sends a POST request with a JSON payload. The response includes the URI of the newly created resource. You can create parent and child records in a single call by nesting related objects in the payload.
curl -- Create a work order with planned materials:
curl -X POST "https://mas-host/maximo/oslc/os/mxwo" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteid": "BEDFORD",
"description": "Replace pump motor - Building A",
"classstructureid": "1007",
"wopriority": 2,
"worktype": "CM",
"wpmaterial": [
{
"itemnum": "PUMP-100",
"itemqty": 1,
"description": "Centrifugal pump motor"
}
]
}'Python -- Create with planned materials and labor:
import requests
BASE_URL = "https://mas-host/maximo/oslc/os"
headers = {
"apikey": "YOUR_API_KEY",
"Content-Type": "application/json"
}
work_order = {
"siteid": "BEDFORD",
"description": "Replace pump motor - Building A",
"classstructureid": "1007",
"wopriority": 2,
"worktype": "CM",
"wpmaterial": [
{
"itemnum": "PUMP-100",
"itemqty": 1,
"description": "Centrifugal pump motor"
}
],
"wplabor": [
{
"laborcode": "SMITH",
"laborhrs": 4,
"craft": "MECHANIC"
}
]
}
response = requests.post(f"{BASE_URL}/mxwo", headers=headers, json=work_order)
if response.status_code == 201:
# The Location header contains the URI of the new record
new_wo_uri = response.headers.get("Location")
print(f"Created work order: {new_wo_uri}")
else:
print(f"Error {response.status_code}: {response.text}")Node.js -- Create a work order:
const axios = require('axios');
const BASE_URL = 'https://mas-host/maximo/oslc/os';
const headers = {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json'
};
async function createWorkOrder() {
const workOrder = {
siteid: 'BEDFORD',
description: 'Replace pump motor - Building A',
classstructureid: '1007',
wopriority: 2,
worktype: 'CM',
wpmaterial: [
{
itemnum: 'PUMP-100',
itemqty: 1,
description: 'Centrifugal pump motor'
}
]
};
try {
const response = await axios.post(`${BASE_URL}/mxwo`, workOrder, { headers });
console.log('Created:', response.headers.location);
return response.headers.location;
} catch (error) {
console.error('Error:', error.response?.status, error.response?.data);
}
}
createWorkOrder();Key points about CREATE:
- A successful POST returns HTTP 201 with a
Locationheader pointing to the new resource. - Nested child records (like
wpmaterialandwplabor) are created atomically with the parent. - Business rules, validation, and workflows fire just as they would through the UI. If a required field is missing, you get a 400 error with a descriptive message.
READ -- GET
Reading records is where you will spend most of your API time. GET requests support rich query parameters for filtering, selecting fields, sorting, and paginating results.
curl -- Get a single work order by ID:
curl -X GET "https://mas-host/maximo/oslc/os/mxwo/12345" \
-H "apikey: YOUR_API_KEY" \
-H "Accept: application/json"curl -- Query work orders with filters:
# Get all open, high-priority work orders at BEDFORD
curl -X GET "https://mas-host/maximo/oslc/os/mxwo?\
oslc.where=siteid=%22BEDFORD%22%20and%20status=%22APPR%22%20and%20wopriority%3C=2&\
oslc.select=wonum,description,status,wopriority,reportdate&\
oslc.pageSize=50&\
oslc.orderBy=%2Breportdate" \
-H "apikey: YOUR_API_KEY" \
-H "Accept: application/json"Python -- Query with readable parameter construction:
import requests
from urllib.parse import quote
BASE_URL = "https://mas-host/maximo/oslc/os"
headers = {"apikey": "YOUR_API_KEY", "Accept": "application/json"}
# Build query parameters clearly
params = {
"oslc.where": 'siteid="BEDFORD" and status="APPR" and wopriority<=2',
"oslc.select": "wonum,description,status,wopriority,reportdate",
"oslc.pageSize": 50,
"oslc.orderBy": "+reportdate"
}
response = requests.get(f"{BASE_URL}/mxwo", headers=headers, params=params)
data = response.json()
for wo in data.get("member", []):
print(f"{wo['wonum']}: {wo['description']} [{wo['status']}]")Node.js -- Query with async/await:
const axios = require('axios');
const BASE_URL = 'https://mas-host/maximo/oslc/os';
const headers = {
'apikey': 'YOUR_API_KEY',
'Accept': 'application/json'
};
async function getOpenWorkOrders() {
const response = await axios.get(`${BASE_URL}/mxwo`, {
headers,
params: {
'oslc.where': 'siteid="BEDFORD" and status="APPR" and wopriority<=2',
'oslc.select': 'wonum,description,status,wopriority,reportdate',
'oslc.pageSize': 50,
'oslc.orderBy': '+reportdate'
}
});
const workOrders = response.data.member || [];
workOrders.forEach(wo => {
console.log(`${wo.wonum}: ${wo.description} [${wo.status}]`);
});
return workOrders;
}
getOpenWorkOrders();UPDATE -- POST with x-method-override or PATCH
MAS supports two approaches for updating records. The PATCH method is the standard HTTP approach. The POST with x-method-override: PATCH header is the alternative when your HTTP client or middleware does not support PATCH.
curl -- Update using PATCH:
curl -X PATCH "https://mas-host/maximo/oslc/os/mxwo/12345" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "x-method-override: PATCH" \
-d '{
"description": "Replace pump motor - Building A (URGENT)",
"wopriority": 1
}'Python -- Update with optimistic locking:
import requests
BASE_URL = "https://mas-host/maximo/oslc/os"
headers = {
"apikey": "YOUR_API_KEY",
"Content-Type": "application/json",
"x-method-override": "PATCH"
}
# First, GET the record to obtain the ETag for optimistic locking
wo_url = f"{BASE_URL}/mxwo/12345"
get_response = requests.get(wo_url, headers={"apikey": "YOUR_API_KEY"})
# Use the ETag in the If-Match header to prevent concurrent update conflicts
etag = get_response.headers.get("ETag")
if etag:
headers["If-Match"] = etag
update_data = {
"description": "Replace pump motor - Building A (URGENT)",
"wopriority": 1
}
response = requests.post(wo_url, headers=headers, json=update_data)
if response.status_code == 204:
print("Work order updated successfully")
elif response.status_code == 412:
print("Conflict: record was modified by another user. Refresh and retry.")
else:
print(f"Error {response.status_code}: {response.text}")Node.js -- Update with error handling:
const axios = require('axios');
const BASE_URL = 'https://mas-host/maximo/oslc/os';
async function updateWorkOrder(woId, updates) {
const woUrl = `${BASE_URL}/mxwo/${woId}`;
// Get current ETag for optimistic locking
const getResponse = await axios.get(woUrl, {
headers: { 'apikey': 'YOUR_API_KEY' }
});
const etag = getResponse.headers.etag;
try {
const response = await axios.post(woUrl, updates, {
headers: {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json',
'x-method-override': 'PATCH',
...(etag && { 'If-Match': etag })
}
});
console.log('Updated successfully');
return true;
} catch (error) {
if (error.response?.status === 412) {
console.error('Conflict detected. Record was modified by another user.');
} else {
console.error('Update failed:', error.response?.data);
}
return false;
}
}
updateWorkOrder('12345', {
description: 'Replace pump motor - Building A (URGENT)',
wopriority: 1
});DELETE -- DELETE
Deleting records is straightforward but carries significant consequences. The API enforces the same referential integrity and business rules as the UI. You cannot delete a work order that has actuals posted against it, for example.
curl:
curl -X DELETE "https://mas-host/maximo/oslc/os/mxwo/12345" \
-H "apikey: YOUR_API_KEY"Python:
response = requests.delete(
f"{BASE_URL}/mxwo/12345",
headers={"apikey": "YOUR_API_KEY"}
)
if response.status_code == 204:
print("Record deleted")
elif response.status_code == 400:
print(f"Cannot delete: {response.json().get('Error', {}).get('message', '')}")Node.js:
async function deleteWorkOrder(woId) {
try {
await axios.delete(`${BASE_URL}/mxwo/${woId}`, {
headers: { 'apikey': 'YOUR_API_KEY' }
});
console.log('Record deleted');
} catch (error) {
if (error.response?.status === 400) {
console.error('Cannot delete:', error.response.data?.Error?.message);
}
}
}Warning: DELETE is permanent. There is no soft-delete in the REST API. If your integration needs to "remove" records, consider changing their status to a closed or cancelled state instead of deleting them.
Advanced Queries with oslc.where
The oslc.where clause is the query engine of the MAS REST API. It supports a rich set of operators that let you build precise queries without over-fetching data. Mastering oslc.where is the difference between an integration that makes 100 API calls and one that makes 5.
Comparison Operators
Operator — Meaning — Example
= — Equal to — status="APPR"
!= — Not equal to — status!="CLOSE"
> — Greater than — wopriority>2
< — Less than — wopriority<3
>= — Greater than or equal — wopriority>=1
<= — Less than or equal — wopriority<=2
Practical Query Examples
Here are ten queries you will use in real integrations, with explanations.
1. Exact match with AND:
oslc.where=siteid="BEDFORD" and status="APPR" and worktype="CM"All approved corrective maintenance work orders at the BEDFORD site.
2. Wildcard search with %:
oslc.where=description like "%pump%" and siteid="BEDFORD"All records where the description contains the word "pump." The % wildcard matches any sequence of characters.
3. OR conditions:
oslc.where=status="APPR" or status="INPRG" or status="WMATL"Work orders in any of three active statuses. Use or to combine multiple conditions when any match is acceptable.
4. Date range queries:
oslc.where=reportdate>="2026-01-01T00:00:00-05:00" and reportdate<"2026-02-01T00:00:00-05:00"All work orders reported in January 2026. Always include the timezone offset in date values to avoid ambiguity.
5. Querying by related record (asset):
oslc.where=assetnum="PUMP-001" and siteid="BEDFORD"All work orders for a specific asset at a specific site. Always include siteid when querying by asset -- asset numbers are unique per site, not globally.
6. Null checks:
oslc.where=targstartdate!=null and targcompdate=nullWork orders that have a target start date but no target completion date. Useful for data quality audits.
7. Numeric range:
oslc.where=wopriority>=1 and wopriority<=3 and siteid="BEDFORD"High and medium priority work orders (priority 1-3).
8. Complex nested conditions:
oslc.where=(status="APPR" or status="INPRG") and wopriority<=2 and worktype="EM"Emergency work orders that are approved or in progress with high priority. Parentheses group the OR condition so it does not affect the AND clauses.
9. Querying by modification date (for delta syncs):
oslc.where=changedate>="2026-02-05T00:00:00-05:00" and siteid="BEDFORD"Records modified since a specific date. This is the foundation of delta synchronization -- pull only what changed since your last sync.
10. Combining wildcards with status filters:
oslc.where=description like "%HVAC%" and (status="WAPPR" or status="APPR") and siteid="BEDFORD"HVAC-related work orders awaiting or already approved.
Field Selection with oslc.select
Do not fetch entire records when you only need three fields. Use oslc.select to reduce response size and improve performance.
oslc.select=wonum,description,status,wopriority,reportdate,assetnumSelecting child records:
oslc.select=wonum,description,status,wpmaterial{itemnum,itemqty,description}This returns work orders with only the specified fields from the parent and the nested planned materials.
Sorting with oslc.orderBy
oslc.orderBy=+wopriority,-reportdateThe + prefix sorts ascending (lowest priority number first). The - prefix sorts descending (most recent report date first). You can chain multiple sort fields.
Putting It All Together
Python -- A complete production-style query:
import requests
BASE_URL = "https://mas-host/maximo/oslc/os"
headers = {"apikey": "YOUR_API_KEY", "Accept": "application/json"}
params = {
"oslc.where": (
'siteid="BEDFORD" '
'and (status="APPR" or status="INPRG") '
'and wopriority<=2 '
'and reportdate>="2026-01-01T00:00:00-05:00"'
),
"oslc.select": "wonum,description,status,wopriority,reportdate,assetnum,location",
"oslc.orderBy": "+wopriority,-reportdate",
"oslc.pageSize": 100
}
response = requests.get(f"{BASE_URL}/mxwo", headers=headers, params=params)
data = response.json()
print(f"Total matching records: {data.get('totalCount', 'unknown')}")
for wo in data.get("member", []):
print(f" [{wo.get('wopriority')}] {wo.get('wonum')}: {wo.get('description')}")Pagination
Never attempt to retrieve all records in a single request. In production environments, queries can match thousands or millions of records. Pagination is not optional -- it is a hard requirement for any integration that will run against real data.
How Pagination Works
The MAS REST API uses oslc.pageSize to control page size and returns a nextPage link in the response when more records are available. You follow the nextPage link until it is no longer present.
Python -- Complete Pagination Implementation
import requests
def get_all_records(base_url, api_key, object_structure, query,
select=None, page_size=100):
"""
Paginate through all results for a given query.
Args:
base_url: MAS base URL (e.g., https://mas-host/maximo/oslc/os)
api_key: API key for authentication
object_structure: The OS to query (e.g., mxwo, mxasset)
query: oslc.where clause
select: Optional oslc.select fields
page_size: Records per page (default 100, max varies by config)
Returns:
List of all matching records
"""
headers = {"apikey": api_key, "Accept": "application/json"}
params = {
"oslc.where": query,
"oslc.pageSize": page_size
}
if select:
params["oslc.select"] = select
url = f"{base_url}/{object_structure}"
all_records = []
page_count = 0
while url:
response = requests.get(url, headers=headers, params=params if page_count == 0 else None)
response.raise_for_status()
data = response.json()
members = data.get("member", [])
all_records.extend(members)
page_count += 1
print(f" Page {page_count}: fetched {len(members)} records "
f"(total: {len(all_records)})")
# Get next page URL from response
response_info = data.get("responseInfo", {})
next_page = response_info.get("nextPage", None)
if next_page:
url = next_page.get("href") if isinstance(next_page, dict) else None
else:
url = None
# Clear params for subsequent pages -- the nextPage URL includes them
params = None
print(f"Pagination complete: {len(all_records)} total records in {page_count} pages")
return all_records
# Usage
records = get_all_records(
base_url="https://mas-host/maximo/oslc/os",
api_key="YOUR_API_KEY",
object_structure="mxwo",
query='siteid="BEDFORD" and status="APPR"',
select="wonum,description,status,wopriority",
page_size=200
)Node.js -- Pagination with Async Generators
const axios = require('axios');
async function* paginateRecords(baseUrl, apiKey, objectStructure, query, options = {}) {
const { select, pageSize = 100 } = options;
const headers = { 'apikey': apiKey, 'Accept': 'application/json' };
let url = `${baseUrl}/${objectStructure}`;
let params = {
'oslc.where': query,
'oslc.pageSize': pageSize,
...(select && { 'oslc.select': select })
};
let isFirstPage = true;
while (url) {
const response = await axios.get(url, {
headers,
...(isFirstPage && { params })
});
const data = response.data;
const members = data.member || [];
// Yield each page of records
yield members;
// Follow next page link
const nextPage = data.responseInfo?.nextPage;
url = nextPage?.href || null;
isFirstPage = false;
}
}
// Usage with async iteration
async function getAllWorkOrders() {
const allRecords = [];
for await (const page of paginateRecords(
'https://mas-host/maximo/oslc/os',
'YOUR_API_KEY',
'mxwo',
'siteid="BEDFORD" and status="APPR"',
{ select: 'wonum,description,status', pageSize: 200 }
)) {
allRecords.push(...page);
console.log(`Fetched page, total records so far: ${allRecords.length}`);
}
return allRecords;
}
getAllWorkOrders().then(records => {
console.log(`Total: ${records.length} work orders`);
});Pagination Best Practices
- Set a reasonable page size. 100-200 records per page is the sweet spot for most integrations. Too small means excessive HTTP overhead. Too large means slow response times and memory pressure.
- Do not use oslc.pageSize without a limit strategy. If you set page size to 5000 and your query matches 500,000 records, you will hammer the server. Consider whether you truly need all records or just a count.
- Use oslc.orderBy with pagination. Without explicit ordering, the order of records across pages is not guaranteed. If your integration depends on processing records in a specific order, specify it.
- Implement a safety valve. Set a maximum page count in your pagination loop to prevent runaway queries from consuming resources indefinitely.
Bulk Operations
When you need to create, update, or delete multiple records, individual API calls are slow and wasteful. Bulk operations let you handle many records in a single HTTP request.
Bulk Create
import requests
BASE_URL = "https://mas-host/maximo/oslc/os"
headers = {
"apikey": "YOUR_API_KEY",
"Content-Type": "application/json"
}
# Bulk create work orders
bulk_payload = {
"member": [
{
"siteid": "BEDFORD",
"description": "PM - HVAC Unit 1",
"worktype": "PM",
"wopriority": 3
},
{
"siteid": "BEDFORD",
"description": "PM - HVAC Unit 2",
"worktype": "PM",
"wopriority": 3
},
{
"siteid": "BEDFORD",
"description": "PM - HVAC Unit 3",
"worktype": "PM",
"wopriority": 3
}
]
}
response = requests.post(
f"{BASE_URL}/mxwo?action=system:createbulk",
headers=headers,
json=bulk_payload
)
if response.status_code == 200:
result = response.json()
print(f"Bulk create successful: {len(result.get('member', []))} records created")
else:
print(f"Error: {response.status_code} - {response.text}")Bulk Update
# Bulk update -- change priority on multiple work orders
bulk_update = {
"member": [
{
"href": "https://mas-host/maximo/oslc/os/mxwo/12345",
"wopriority": 1
},
{
"href": "https://mas-host/maximo/oslc/os/mxwo/12346",
"wopriority": 1
},
{
"href": "https://mas-host/maximo/oslc/os/mxwo/12347",
"wopriority": 1
}
]
}
response = requests.post(
f"{BASE_URL}/mxwo?action=system:updatebulk",
headers=headers,
json=bulk_update
)Node.js -- Bulk Create with Batching
For very large datasets, batch your bulk operations to avoid timeouts:
const axios = require('axios');
async function bulkCreateInBatches(records, batchSize = 50) {
const BASE_URL = 'https://mas-host/maximo/oslc/os';
const headers = {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json'
};
const results = { created: 0, failed: 0, errors: [] };
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);
const batchNumber = Math.floor(i / batchSize) + 1;
try {
const response = await axios.post(
`${BASE_URL}/mxwo?action=system:createbulk`,
{ member: batch },
{ headers }
);
results.created += batch.length;
console.log(`Batch ${batchNumber}: ${batch.length} records created`);
} catch (error) {
results.failed += batch.length;
results.errors.push({
batch: batchNumber,
error: error.response?.data || error.message
});
console.error(`Batch ${batchNumber} failed:`, error.response?.status);
}
}
console.log(`\nBulk create complete: ${results.created} created, ${results.failed} failed`);
return results;
}
// Generate 200 PM work orders
const workOrders = Array.from({ length: 200 }, (_, i) => ({
siteid: 'BEDFORD',
description: `PM - Equipment ${String(i + 1).padStart(3, '0')}`,
worktype: 'PM',
wopriority: 3
}));
bulkCreateInBatches(workOrders, 50);Bulk operation limits: MAS has configurable limits on bulk operation size. The default is typically 100-200 records per request. Exceeding this limit returns a 400 error. Check your environment's mxe.rest.bulkcount system property for the exact limit.
Action Invocations
The MAS REST API does not just handle data -- it can trigger Maximo business actions. Status changes, approvals, and custom automation scripts can all be invoked through the API.
Changing Work Order Status
Status changes in Maximo are not simple field updates. They trigger workflow routing, status validation, and business rules. Use the action parameter to invoke the status change action properly.
curl:
# Change work order status from APPR to INPRG
curl -X POST "https://mas-host/maximo/oslc/os/mxwo/12345?action=wsmethod:changeStatus" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "INPRG",
"memo": "Work started per dispatcher assignment"
}'Python -- Status change with validation:
import requests
def change_wo_status(base_url, api_key, wo_id, new_status, memo=""):
"""
Change work order status using the proper action invocation.
This fires all business rules, workflows, and escalations.
"""
headers = {
"apikey": api_key,
"Content-Type": "application/json"
}
url = f"{base_url}/mxwo/{wo_id}?action=wsmethod:changeStatus"
payload = {
"status": new_status,
"memo": memo
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code in [200, 204]:
print(f"Status changed to {new_status}")
return True
elif response.status_code == 400:
error = response.json().get("Error", {})
print(f"Status change rejected: {error.get('message', 'Unknown error')}")
return False
else:
print(f"Unexpected error: {response.status_code}")
return False
# Usage
change_wo_status(
base_url="https://mas-host/maximo/oslc/os",
api_key="YOUR_API_KEY",
wo_id="12345",
new_status="INPRG",
memo="Technician en route"
)Approving a Purchase Order
def approve_purchase_order(base_url, api_key, po_id):
"""Approve a PO via API action invocation."""
headers = {
"apikey": api_key,
"Content-Type": "application/json"
}
url = f"{base_url}/mxpo/{po_id}?action=wsmethod:changeStatus"
payload = {
"status": "APPR",
"memo": "Approved via automated integration"
}
response = requests.post(url, headers=headers, json=payload)
return response.status_code in [200, 204]Invoking Custom Automation Scripts
If you have automation scripts configured with action launch points, you can invoke them through the API:
# Invoke a custom action defined as an automation script
def invoke_custom_action(base_url, api_key, wo_id, action_name, params=None):
"""
Invoke a custom automation script action on a work order.
The action_name must match the action configured in the
automation script's action launch point.
"""
headers = {
"apikey": api_key,
"Content-Type": "application/json"
}
url = f"{base_url}/mxwo/{wo_id}?action=wsmethod:{action_name}"
response = requests.post(url, headers=headers, json=params or {})
if response.status_code in [200, 204]:
print(f"Action '{action_name}' executed successfully")
return True
else:
print(f"Action failed: {response.status_code} - {response.text}")
return False
# Example: invoke a custom "assignCrew" action
invoke_custom_action(
base_url="https://mas-host/maximo/oslc/os",
api_key="YOUR_API_KEY",
wo_id="12345",
action_name="assignCrew",
params={"crewid": "CREW-ALPHA", "schedstart": "2026-02-10T08:00:00-05:00"}
)Important: Action invocations execute the same business logic as performing the action through the UI. If a workflow requires approval before a status change, the API call will either route to the workflow or reject the change -- depending on the workflow configuration. The API does not bypass business rules.
Attachments
Maximo's DOCLINKS system manages file attachments for records. The REST API supports uploading, downloading, and listing attachments on any record that supports document links.
Uploading an Attachment
Python -- Upload a file to a work order:
import requests
def upload_attachment(base_url, api_key, wo_href, file_path,
description, doc_type="Attachments"):
"""
Upload a file attachment to a work order.
Args:
base_url: MAS base URL
api_key: API key
wo_href: Full href of the work order resource
file_path: Local path to the file
description: Description of the attachment
doc_type: Document type (default: Attachments)
"""
import os
import mimetypes
filename = os.path.basename(file_path)
mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
headers = {
"apikey": api_key,
"slug": filename,
"x-document-meta": f"FILE/{doc_type}",
"x-document-description": description
}
with open(file_path, "rb") as f:
response = requests.post(
f"{wo_href}/doclinks",
headers={**headers, "Content-Type": mime_type},
data=f
)
if response.status_code == 201:
print(f"Uploaded '{filename}' successfully")
return response.headers.get("Location")
else:
print(f"Upload failed: {response.status_code} - {response.text}")
return None
# Usage
upload_attachment(
base_url="https://mas-host/maximo/oslc/os",
api_key="YOUR_API_KEY",
wo_href="https://mas-host/maximo/oslc/os/mxwo/12345",
file_path="/path/to/inspection_report.pdf",
description="Q1 2026 Pump Inspection Report"
)Listing Attachments
def list_attachments(base_url, api_key, wo_href):
"""List all attachments on a work order."""
headers = {"apikey": api_key, "Accept": "application/json"}
response = requests.get(f"{wo_href}/doclinks", headers=headers)
data = response.json()
for doc in data.get("member", []):
print(f" - {doc.get('describedBy', {}).get('fileName', 'Unknown')}: "
f"{doc.get('describedBy', {}).get('description', 'No description')}")
return data.get("member", [])Downloading an Attachment
def download_attachment(api_key, attachment_href, save_path):
"""Download an attachment by its href."""
headers = {"apikey": api_key}
response = requests.get(attachment_href, headers=headers, stream=True)
if response.status_code == 200:
with open(save_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Downloaded to {save_path}")
else:
print(f"Download failed: {response.status_code}")
# Usage: List then download
attachments = list_attachments(
base_url="https://mas-host/maximo/oslc/os",
api_key="YOUR_API_KEY",
wo_href="https://mas-host/maximo/oslc/os/mxwo/12345"
)
if attachments:
download_attachment(
api_key="YOUR_API_KEY",
attachment_href=attachments[0]["href"],
save_path="/tmp/downloaded_report.pdf"
)Error Handling and Retry Logic
Production integrations fail. Networks drop. Servers restart. Rate limits trigger. The difference between a fragile integration and a resilient one is how it handles these failures.
The Resilient API Client
Python -- Production-grade API client with retry logic:
import time
import logging
from requests.exceptions import RequestException, ConnectionError, Timeout
import requests
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("maximo_api")
def resilient_api_call(method, url, headers, max_retries=3,
timeout=30, **kwargs):
"""
Make an API call with exponential backoff retry logic.
Handles:
- Network errors (ConnectionError, Timeout)
- Rate limiting (429)
- Server errors (500, 502, 503, 504)
- Transient failures
Does NOT retry:
- Client errors (400, 401, 403, 404, 409) -- these need human intervention
"""
retryable_status_codes = {429, 500, 502, 503, 504}
for attempt in range(max_retries):
try:
response = requests.request(
method, url, headers=headers, timeout=timeout, **kwargs
)
# Success
if response.status_code < 400:
return response
# Rate limited -- respect Retry-After header if present
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
wait = int(retry_after) if retry_after else (2 ** attempt * 5)
logger.warning(f"Rate limited. Waiting {wait}s before retry "
f"(attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
continue
# Server error -- retry with backoff
if response.status_code in retryable_status_codes:
wait = 2 ** attempt * 2
logger.warning(f"Server error {response.status_code}. "
f"Waiting {wait}s (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
continue
# Client error -- do not retry, raise immediately
logger.error(f"Client error {response.status_code}: {response.text[:500]}")
response.raise_for_status()
except (ConnectionError, Timeout) as e:
if attempt == max_retries - 1:
logger.error(f"All {max_retries} attempts failed. Last error: {e}")
raise
wait = 2 ** attempt * 2
logger.warning(f"Connection error: {e}. "
f"Waiting {wait}s (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
except RequestException as e:
logger.error(f"Unexpected request error: {e}")
raise
# Should not reach here, but just in case
raise Exception(f"API call failed after {max_retries} attempts")
# Usage
headers = {"apikey": "YOUR_API_KEY", "Accept": "application/json"}
# GET with retry
response = resilient_api_call(
"GET",
"https://mas-host/maximo/oslc/os/mxwo?oslc.pageSize=10",
headers=headers
)
print(response.json())
# POST with retry
response = resilient_api_call(
"POST",
"https://mas-host/maximo/oslc/os/mxwo",
headers={**headers, "Content-Type": "application/json"},
json={"siteid": "BEDFORD", "description": "Test WO", "worktype": "CM"}
)Node.js -- Resilient Client with axios-retry
const axios = require('axios');
const axiosRetry = require('axios-retry').default;
// Configure axios instance with retry logic
const maximoClient = axios.create({
baseURL: 'https://mas-host/maximo/oslc/os',
timeout: 30000,
headers: {
'apikey': 'YOUR_API_KEY',
'Accept': 'application/json'
}
});
// Configure retry behavior
axiosRetry(maximoClient, {
retries: 3,
retryDelay: (retryCount) => {
const delay = Math.pow(2, retryCount) * 1000;
console.log(`Retry attempt ${retryCount}, waiting ${delay}ms`);
return delay;
},
retryCondition: (error) => {
// Retry on network errors and specific status codes
return (
axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 429 ||
error.response?.status >= 500
);
}
});
// Usage
async function getWorkOrders() {
try {
const response = await maximoClient.get('/mxwo', {
params: {
'oslc.where': 'siteid="BEDFORD" and status="APPR"',
'oslc.pageSize': 50
}
});
return response.data;
} catch (error) {
console.error('All retries exhausted:', error.message);
throw error;
}
}Common HTTP Status Codes in MAS Context
Understanding what each status code means in the Maximo context saves hours of debugging.
Status Code — Meaning — MAS Context
200 OK — Request succeeded — Record(s) returned successfully, action completed
201 Created — Resource created — New record created; check Location header for URI
204 No Content — Success, no body — Update or delete completed successfully
400 Bad Request — Invalid request — Missing required field, validation rule failed, invalid query syntax
401 Unauthorized — Authentication failed — Invalid or expired API key/token
403 Forbidden — Authorization failed — User lacks permission for this object/site/action
404 Not Found — Resource not found — Record does not exist or object structure is wrong
405 Method Not Allowed — HTTP method not supported — Trying DELETE on a read-only object structure
409 Conflict — Conflict detected — Duplicate key violation (e.g., creating a record with an existing ID)
412 Precondition Failed — ETag mismatch — Record was modified since you last read it (optimistic locking)
429 Too Many Requests — Rate limit exceeded — Slow down; implement backoff
500 Internal Server Error — Server-side failure — Maximo application error; check server logs
502 Bad Gateway — Gateway error — Load balancer or reverse proxy issue
503 Service Unavailable — Server overloaded — MAS pod is restarting or under heavy load
Parsing Error Responses
MAS error responses follow a consistent JSON structure. Here is how to extract useful information:
def parse_maximo_error(response):
"""
Parse a MAS error response and return a structured error object.
"""
try:
error_body = response.json()
error_info = error_body.get("Error", {})
return {
"status_code": response.status_code,
"error_code": error_info.get("reasonCode", "UNKNOWN"),
"message": error_info.get("message", "No message provided"),
"status_description": error_info.get("statusDescription", ""),
"extended_error": error_info.get("extendedError", {})
}
except ValueError:
return {
"status_code": response.status_code,
"error_code": "PARSE_ERROR",
"message": response.text[:500],
"status_description": "",
"extended_error": {}
}
# Usage
response = requests.post(url, headers=headers, json=bad_payload)
if response.status_code >= 400:
error = parse_maximo_error(response)
print(f"[{error['error_code']}] {error['message']}")Performance Tips
The difference between an integration that runs in 30 seconds and one that runs in 30 minutes often comes down to these optimizations.
1. Use oslc.select to Minimize Response Size
Every field you do not need is wasted bandwidth and processing time. A full work order record with all related objects can be 50+ KB. Selecting only the fields you need can reduce that to 1-2 KB.
# Bad: fetches everything
GET /maximo/oslc/os/mxwo?oslc.where=siteid="BEDFORD"
# Good: fetches only what you need
GET /maximo/oslc/os/mxwo?oslc.where=siteid="BEDFORD"&oslc.select=wonum,status,description2. Right-Size Your Page Size
Scenario — Recommended Page Size
Interactive UI queries — 20-50
Background sync jobs — 100-200
Bulk data export — 500 (if your server config allows)
Record count only — Use oslc.pageSize=1 with totalCount
3. Use Connection Pooling
Creating a new HTTP connection for every request adds 50-200ms of overhead. Use connection pooling with persistent sessions.
Python:
import requests
# Create a session that reuses connections
session = requests.Session()
session.headers.update({
"apikey": "YOUR_API_KEY",
"Accept": "application/json"
})
# All requests through the session reuse the TCP connection
response1 = session.get("https://mas-host/maximo/oslc/os/mxwo?oslc.pageSize=10")
response2 = session.get("https://mas-host/maximo/oslc/os/mxasset?oslc.pageSize=10")
response3 = session.get("https://mas-host/maximo/oslc/os/mxsr?oslc.pageSize=10")Node.js:
const https = require('https');
const axios = require('axios');
// Create an axios instance with a keep-alive agent
const agent = new https.Agent({
keepAlive: true,
maxSockets: 10
});
const client = axios.create({
baseURL: 'https://mas-host/maximo/oslc/os',
httpsAgent: agent,
headers: { 'apikey': 'YOUR_API_KEY', 'Accept': 'application/json' }
});4. Implement Delta Synchronization
Do not pull all records every time. Track the last sync timestamp and query only records that changed since then.
from datetime import datetime, timezone
def delta_sync(base_url, api_key, object_structure, last_sync_time):
"""
Fetch only records modified since last sync.
Args:
last_sync_time: ISO 8601 timestamp of last successful sync
"""
headers = {"apikey": api_key, "Accept": "application/json"}
query = f'changedate>="{last_sync_time}"'
params = {
"oslc.where": query,
"oslc.orderBy": "+changedate",
"oslc.pageSize": 200
}
response = requests.get(
f"{base_url}/{object_structure}",
headers=headers,
params=params
)
data = response.json()
records = data.get("member", [])
# Update sync timestamp to now
new_sync_time = datetime.now(timezone.utc).isoformat()
return records, new_sync_time5. Cache Reference Data
Data that rarely changes -- locations, item masters, classification structures -- should be cached locally rather than queried on every transaction.
import time
class MaximoCache:
"""Simple TTL cache for Maximo reference data."""
def __init__(self, ttl_seconds=3600):
self._cache = {}
self._ttl = ttl_seconds
def get(self, key):
if key in self._cache:
data, timestamp = self._cache[key]
if time.time() - timestamp < self._ttl:
return data
del self._cache[key]
return None
def set(self, key, data):
self._cache[key] = (data, time.time())
# Usage
cache = MaximoCache(ttl_seconds=3600) # 1-hour TTL
def get_locations(base_url, api_key, siteid):
cache_key = f"locations_{siteid}"
cached = cache.get(cache_key)
if cached:
return cached
headers = {"apikey": api_key, "Accept": "application/json"}
response = requests.get(
f"{base_url}/mxloc",
headers=headers,
params={"oslc.where": f'siteid="{siteid}"', "oslc.pageSize": 1000}
)
locations = response.json().get("member", [])
cache.set(cache_key, locations)
return locations6. Use Lean Mode for Minimal Responses
Add the lean parameter to reduce response payload by removing namespace prefixes and metadata:
GET /maximo/oslc/os/mxwo?lean=1&oslc.pageSize=10This can reduce response size by 20-40% for large result sets.
Quick Reference Card
Bookmark this section. It contains the URL patterns and parameters you will use most often.
URL Patterns
Operation — HTTP Method — URL Pattern
List / Query — GET — /maximo/oslc/os/{os}?oslc.where=...
Get by ID — GET — /maximo/oslc/os/{os}/{id}
Create — POST — /maximo/oslc/os/{os}
Update — POST + x-method-override — /maximo/oslc/os/{os}/{id}
Delete — DELETE — /maximo/oslc/os/{os}/{id}
Bulk Create — POST — /maximo/oslc/os/{os}?action=system:createbulk
Bulk Update — POST — /maximo/oslc/os/{os}?action=system:updatebulk
Change Status — POST — /maximo/oslc/os/{os}/{id}?action=wsmethod:changeStatus
Custom Action — POST — /maximo/oslc/os/{os}/{id}?action=wsmethod:{action}
List Attachments — GET — /maximo/oslc/os/{os}/{id}/doclinks
Upload Attachment — POST — /maximo/oslc/os/{os}/{id}/doclinks
Query Parameters
Parameter — Purpose — Example
oslc.where — Filter records — status="APPR" and siteid="BEDFORD"
oslc.select — Choose fields — wonum,description,status
oslc.orderBy — Sort results — +wopriority,-reportdate
oslc.pageSize — Page size — 100
lean — Minimize response — 1
action — Invoke action — wsmethod:changeStatus
Required Headers
Header — Purpose — Example Value
apikey — API key auth — a1b2c3d4e5f6g7h8i9j0
Authorization — OAuth auth — Bearer eyJhbGciOi...
Content-Type — Request body format — application/json
Accept — Response format — application/json
x-method-override — Override HTTP method — PATCH
If-Match — Optimistic locking — ETag value from GET
slug — Attachment filename — report.pdf
x-document-meta — Attachment doc type — FILE/Attachments
Key Takeaways
- API key authentication is the simplest method for server-to-server integrations. OAuth 2.0 is the right choice when you need token expiration, user context, or compliance with enterprise security policies.
- The oslc.where clause is your query engine. It supports comparisons, wildcards, AND/OR logic, date ranges, and null checks. Master it and you will eliminate most unnecessary API calls.
- Bulk operations are essential for performance. Creating 100 work orders one at a time means 100 HTTP round trips. A single bulk create request does the same work in one call.
- Always implement pagination, error handling, and retry logic in production integrations. The happy path works in development. Production requires resilience.
- The REST API enforces business logic. Validation rules, workflows, escalations, and security -- everything that fires through the UI fires through the API. This is a feature, not a limitation. It means your integrations maintain data integrity by default.
- Performance optimization is not premature. Using
oslc.select, connection pooling, delta synchronization, and caching from the start prevents the painful refactoring that comes when your integration hits production data volumes.
References
- IBM Maximo REST API Documentation
- IBM Maximo Application Suite API Reference
- OASIS OSLC Core Specification v3.0
- IBM Maximo NextGen REST API Guide
- OAuth 2.0 Client Credentials Grant (RFC 6749)
- OpenID Connect Discovery Specification
- axios-retry Documentation
- Python requests Session Documentation
Series Navigation
Previous: — Part 3 -- From Publish Channels to Events: The Event-Driven Transformation
Next: — Part 5 -- Enterprise Integration Patterns: App Connect, Kafka, and Beyond
View the full MAS INTEGRATION series index
Part 4 of the "MAS INTEGRATION" series | Published by TheMaximoGuys



