POST /IDM/api/items./items/{pid}/resource/stream calls for file retrieval. [src7]Infor Document Management (IDM) is a cloud-based enterprise content management (ECM) system that is part of the Infor OS platform. It provides document storage, versioning, search, and lifecycle management capabilities for all Infor CloudSuite applications (M3, LN, Syteline, etc.). IDM is accessed exclusively through the Infor ION API Gateway — there is no direct API endpoint outside the gateway. This card covers the cloud deployment; on-premises Infor OS deployments share the same REST API surface but have different infrastructure constraints. [src2, src3]
| Property | Value |
|---|---|
| Vendor | Infor |
| System | Infor Document Management (IDM), Infor OS 2024.x/2025.x |
| API Surface | REST (JSON/XML) via ION API Gateway |
| Current API Version | IDM API v1 (no formal versioning; tied to Infor OS release) |
| Editions Covered | Essentials (15K docs), Professional (75K docs), Enterprise (300K docs) |
| Deployment | Cloud (Infor CloudSuite) |
| API Docs | Infor Developer Portal — Document Management |
| Status | GA |
IDM exposes a single REST API surface through the ION API Gateway. All endpoints are prefixed with the tenant-specific gateway URL. The API is organized around the Items resource (documents). [src2, src3]
| API Surface | Protocol | Best For | Max Payload | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
| IDM REST API (Items) | HTTPS/JSON or XML | Document CRUD, metadata management | 2 GB per file (base64) | Tier-dependent: 75K-1.5M ops/day | Yes | No |
| IDM Items Search | HTTPS/JSON or XML | Document discovery via XQuery | Response can be very large | Tier-dependent: 75K-1.5M searches/day | Yes | No |
| IDM Resource Stream | HTTPS/binary | File content download | 2 GB | Shared with retrieval quota | Yes | No |
| IDM Utilities (Batch) | CLI/GUI tool | Bulk import, export, batch update | N/A (tool-based) | Not API-rate-limited | No | Yes |
| ION Document Flows | ION Process Designer | Event-driven document routing | Per-BOD limits | ION message limits | Event-driven | Yes |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max file size per upload | 2 GB | POST /IDM/api/items | Global system limit; base64 encoding increases payload by ~33% [src1] |
| Max file size for Copy Document | 50 MB | Copy operations only | Much lower than standard upload [src1] |
| Upload timeout | 30 minutes | All upload operations | Connection drops if exceeded [src1] |
| Background upload threshold | 20 MB (default, configurable) | Large file uploads | Files above threshold are processed asynchronously [src1] |
| Large file API threshold | 2 MB (default, configurable) | Resource stream | Files above this use the optimized large file API [src2] |
| Search pagination | offset + limit params | /IDM/api/items/search | No server-side cursor; offset-based only [src7] |
| Limit Type | Essentials (1-74,999) | Professional (75K-299,999) | Enterprise (300K+) | Peak Rate |
|---|---|---|---|---|
| Document retrievals | 75,000/day | 750,000/day | 1,500,000/day | 1,000-2,500/min |
| Document processing (upload/update) | 75,000/day | 750,000/day | 1,500,000/day | 600-2,000/min |
| Searches | 75,000/day | 750,000/day | 1,500,000/day | 1,000-2,500/min |
Expansion available via ION-S-DM add-on SKU in 15,000-unit increments. [src1]
All IDM API calls are routed through the Infor ION API Gateway, which handles OAuth 2.0 authentication. There is no way to call the IDM API directly without going through the gateway. [src5, src8]
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Resource Owner Grant (password) | Backend/server-to-server integrations, no user present | ~2h (default, configurable per app) | Yes (30-day grant lifetime default) | Uses service account accessKey/secretKey. Recommended for integrations. [src8] |
| Authorization Code Grant | Web/mobile apps with user interaction | ~2h (default) | Yes | User authorizes app; requires callback URL. [src5] |
| SAML Bearer Grant | Apps already authenticated within Infor OS/Ming.le | Session-bound | No | Reuses existing SSO token. [src6] |
| Implicit Grant | Single-page apps (legacy) | ~2h | No | Not recommended for new development. [src6] |
accessKey as username and secretKey as password, NOT a human user's login. Generated from ION API Authorized Apps configuration. [src5, src8]ci (client ID), cs (client secret), saak (access key), sask (secret key), iu (ION API base URL), pu (portal URL), oa (OAuth token endpoint). [src6]START — User needs to integrate with Infor IDM
|-- What's the operation?
| |-- Upload single document
| | |-- File size < 20 MB? --> POST /IDM/api/items (synchronous)
| | |-- File < 2 GB? --> POST /IDM/api/items (background processing)
| | |-- File > 2 GB? --> Split or use IDM Utilities bulk import
| |-- Search for documents
| | |-- Metadata only? --> GET /IDM/api/items/search + separate /resource/stream
| | |-- Large result set? --> Use $offset and $limit pagination
| |-- Download document --> GET /IDM/api/items/{pid}/resource/stream
| |-- Bulk import --> Use IDM Utilities File Import tool
| |-- Event-driven --> Use ION Document Flows
|-- Authentication
| |-- Backend integration --> Resource Owner grant with service account
| |-- Web/mobile app --> Authorization Code grant
| |-- Inside Infor OS --> SAML Bearer grant
|-- Error tolerance?
|-- Zero-loss --> Attribute-based duplicate checks before upload
|-- Best-effort --> Fire-and-forget with retry on 4xx/5xx
| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| Upload document | POST | /IDM/api/items | JSON/XML with base64 file | Requires entityName, attrs, resrs, acl [src4] |
| Search documents | GET | /IDM/api/items/search | XQuery in query param | Use $offset/$limit for pagination [src7] |
| Get document metadata | GET | /IDM/api/items/{pid} | N/A | Returns attributes and version info [src2] |
| Download file content | GET | /IDM/api/items/{pid}/resource/stream | N/A | Returns binary stream [src7] |
| Check out document | POST | /IDM/api/items/{pid}/checkout | N/A | Locks for exclusive editing [src2] |
| Check in document | POST | /IDM/api/items/{pid}/checkin | Updated file content | Creates new major version [src2] |
| Discard checkout | POST | /IDM/api/items/{pid}/discardCheckout | N/A | Releases lock without changes [src2] |
| Delete document | DELETE | /IDM/api/items/{pid} | N/A | Requires appropriate ACL [src2] |
| Update metadata | PUT | /IDM/api/items/{pid} | JSON/XML with updated attrs | No new version created [src2] |
Navigate to Infor ION Desk > ION API > Authorized Apps. Create a new Backend Service type authorized app. Download the .ionapi credentials file containing client_id, client_secret, service account keys, and OAuth token endpoint URL. [src5, src6]
// The .ionapi file structure:
{
"ci": "CLIENT_ID",
"cs": "CLIENT_SECRET",
"saak": "SERVICE_ACCOUNT_ACCESS_KEY",
"sask": "SERVICE_ACCOUNT_SECRET_KEY",
"iu": "https://{tenant}.ionapi.infor.com",
"pu": "https://{tenant}.portal.infor.com",
"oa": "https://{tenant}.portal.infor.com/IONSERVICES/oauth/token"
}
Verify: The .ionapi file downloads and contains all required fields.
Use the Resource Owner grant to exchange service account credentials for an access token. [src8]
TOKEN=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "username=${SERVICE_ACCOUNT_ACCESS_KEY}" \
-d "password=${SERVICE_ACCOUNT_SECRET_KEY}" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
| jq -r '.access_token')
Verify: echo $TOKEN returns a non-empty JWT string. Token valid for ~2 hours.
Post a document with metadata attributes, base64-encoded file content, document type (entityName), and ACL. [src4]
FILE_B64=$(base64 -w 0 invoice.pdf)
curl -s -X POST "${ION_API_BASE}/IDM/api/items" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"item": {
"attrs": {"attr": [
{"name": "Company_Number", "value": "1000"},
{"name": "Invoice_Number", "value": "INV-2026-0042"}
]},
"resrs": {"res": [{"filename": "invoice.pdf", "base64": "'${FILE_B64}'"}]},
"acl": {"name": "Public"},
"entityName": "Invoice"
}
}' | jq .
Verify: Response contains a pid (persistent item ID).
Use XQuery syntax on the search endpoint. Build queries using the IDM Query Builder UI first, then toggle "Enter Query Manually" to see generated XQuery. [src7]
curl -s -G "${ION_API_BASE}/IDM/api/items/search" \
--data-urlencode "q=/Invoice[@Company_Number = '1000']" \
--data-urlencode "\$offset=0" \
--data-urlencode "\$limit=10" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/json" | jq .
Verify: Response contains matching items with pid values.
Retrieve binary file content via the resource stream endpoint. [src7]
curl -s "${ION_API_BASE}/IDM/api/items/${DOCUMENT_PID}/resource/stream" \
-H "Authorization: Bearer ${TOKEN}" \
-o downloaded_document.pdf
Verify: Downloaded file matches original size and opens correctly.
Handle expired tokens (401), rate limits (429), and upload timeouts. Proactively refresh tokens before the ~2h expiry. [src5, src8]
# Re-acquire token on 401; back off on 429
# For uploads > 30 min timeout, consider chunking or IDM Utilities
Verify: Integration handles 401 by re-authenticating without manual intervention.
# Input: .ionapi credentials file path, document file path, document type, attributes
# Output: Created document PID in IDM
import json, base64
import requests # requests==2.31.0
def get_idm_token(ionapi_path):
with open(ionapi_path) as f:
creds = json.load(f)
resp = requests.post(creds["oa"], data={
"grant_type": "password",
"username": creds["saak"], "password": creds["sask"],
"client_id": creds["ci"], "client_secret": creds["cs"],
})
resp.raise_for_status()
return creds["iu"], resp.json()["access_token"]
def upload_document(ionapi_path, file_path, entity_name, attributes, acl="Public"):
base_url, token = get_idm_token(ionapi_path)
with open(file_path, "rb") as f:
file_b64 = base64.b64encode(f.read()).decode("utf-8")
filename = file_path.rsplit("/", 1)[-1]
payload = {"item": {
"attrs": {"attr": [{"name": k, "value": v} for k, v in attributes.items()]},
"resrs": {"res": [{"filename": filename, "base64": file_b64}]},
"acl": {"name": acl}, "entityName": entity_name,
}}
resp = requests.post(f"{base_url}/IDM/api/items",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json=payload, timeout=1800)
resp.raise_for_status()
return resp.json()
result = upload_document("credentials.ionapi", "invoice.pdf", "Invoice",
{"Company_Number": "1000", "Invoice_Number": "INV-2026-0042"})
print(f"Document created: PID={result.get('pid')}")
// Input: .ionapi credentials, XQuery search string
// Output: Downloaded files saved to disk
// npm install [email protected]
const axios = require("axios");
const fs = require("fs");
async function getToken(creds) {
const resp = await axios.post(creds.oa, new URLSearchParams({
grant_type: "password", username: creds.saak, password: creds.sask,
client_id: creds.ci, client_secret: creds.cs,
}), { headers: { "Content-Type": "application/x-www-form-urlencoded" } });
return resp.data.access_token;
}
async function searchAndDownload(ionapiPath, xquery, outputDir) {
const creds = JSON.parse(fs.readFileSync(ionapiPath, "utf-8"));
const token = await getToken(creds);
const searchResp = await axios.get(`${creds.iu}/IDM/api/items/search`, {
params: { q: xquery, $offset: 0, $limit: 50 },
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
});
for (const item of searchResp.data.items || []) {
const fileResp = await axios.get(
`${creds.iu}/IDM/api/items/${item.pid}/resource/stream`,
{ headers: { Authorization: `Bearer ${token}` }, responseType: "arraybuffer" });
fs.writeFileSync(`${outputDir}/${item.filename || item.pid}`, fileResp.data);
}
}
searchAndDownload("credentials.ionapi", "/Invoice[@Company_Number='1000']", "./downloads")
.catch(console.error);
# Input: .ionapi credentials file
# Output: Token + document search results
CI=$(jq -r '.ci' credentials.ionapi)
CS=$(jq -r '.cs' credentials.ionapi)
SAAK=$(jq -r '.saak' credentials.ionapi)
SASK=$(jq -r '.sask' credentials.ionapi)
OA=$(jq -r '.oa' credentials.ionapi)
IU=$(jq -r '.iu' credentials.ionapi)
TOKEN=$(curl -s -X POST "$OA" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&username=$SAAK&password=$SASK&client_id=$CI&client_secret=$CS" \
| jq -r '.access_token')
curl -s -G "$IU/IDM/api/items/search" \
--data-urlencode "q=/Invoice[@Company_Number = '1000']" \
--data-urlencode "\$offset=0" --data-urlencode "\$limit=5" \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json" | jq '.items | length'
# Expected: number of matching documents
| Field Path | Type | Required | Description | Gotcha |
|---|---|---|---|---|
| item.entityName | String | Yes | Document Type name (must match IDM admin config) | Case-sensitive [src4] |
| item.attrs.attr[].name | String | Yes | Attribute name as defined on Document Type | Must match exactly [src4] |
| item.attrs.attr[].value | String | Yes | Attribute value | All values are strings, even numbers/dates [src4] |
| item.resrs.res[].filename | String | Yes | File name with extension | Extension determines MIME type [src4] |
| item.resrs.res[].base64 | String | Yes | Base64-encoded file content | ~33% size inflation [src1] |
| item.acl.name | String | Yes | Access Control List name | Must be pre-configured in IDM [src4] |
| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 | Unauthorized | Expired or invalid OAuth token | Re-acquire token via Resource Owner grant [src5] |
| 403 | Forbidden | Insufficient ION Desk / API Gateway roles | Grant IONDeskAdmin and IONAPI-Administrator roles [src3] |
| 400 | Bad Request | Missing required attributes or invalid entityName | Verify Document Type config; check case sensitivity [src4] |
| 404 | Not Found | Invalid PID or deleted document | Search by attributes first [src2] |
| 409 | Conflict | Document is checked out by another user | Wait for check-in or discard checkout [src2] |
| 413 | Payload Too Large | File exceeds 2 GB | Split file or use IDM Utilities [src1] |
| 408 | Request Timeout | Upload exceeded 30-minute timeout | Reduce file size; check bandwidth [src1] |
| 429 | Too Many Requests | Exceeded peak rate for subscription tier | Exponential backoff; check tier limits [src1] |
Always use $limit; fetch file content separately via /items/{pid}/resource/stream. [src7]Check expiry before each batch; re-acquire proactively at 90% of lifetime. [src8]Check Document Type config in IDM Administration for all required attributes before building payload. [src4]Monitor for stale checkouts; use discard checkout as admin override. [src2]Set grant lifetime to 0 for indefinite validity, or implement keep-alive. [src8]Dead letter queue for failed ops; replay on recovery. Monitor gateway health independently. [src5]# BAD -- search returns base64 content for every match, creating enormous payloads
results = requests.get(f"{base_url}/IDM/api/items/search",
params={"q": "/Invoice[@Company_Number = '1000']"}, headers=headers)
for item in results.json()["items"]:
file_content = base64.b64decode(item["base64"])
# GOOD -- search with pagination, fetch files via stream endpoint
results = requests.get(f"{base_url}/IDM/api/items/search",
params={"q": "/Invoice[@Company_Number = '1000']", "$offset": 0, "$limit": 50},
headers=headers)
for item in results.json()["items"]:
file_resp = requests.get(
f"{base_url}/IDM/api/items/{item['pid']}/resource/stream", headers=headers)
with open(f"downloads/{item['filename']}", "wb") as f:
f.write(file_resp.content)
# BAD -- no timeout, no retry for large files
requests.post(f"{base_url}/IDM/api/items", json=payload_500mb, headers=headers)
# GOOD -- explicit timeout matching IDM's 30-min limit, with retry
import time
def upload_with_retry(url, payload, headers, max_retries=3):
for attempt in range(max_retries):
try:
resp = requests.post(url, json=payload, headers=headers, timeout=1800)
resp.raise_for_status()
return resp.json()
except requests.Timeout:
if attempt < max_retries - 1:
time.sleep(2 ** attempt * 10)
headers["Authorization"] = f"Bearer {get_fresh_token()}"
else:
raise
# BAD -- attribute names and entityName are case-sensitive
payload = {"item": {"attrs": {"attr": [
{"name": "Company", "value": "1000"}]},
"entityName": "invoice"}}
# GOOD -- validate attribute names and entityName against known schema
DOCUMENT_TYPES = {"Invoice": {"required": ["Company_Number", "Invoice_Number"]}}
def build_payload(entity_name, attrs, filename, file_b64):
schema = DOCUMENT_TYPES.get(entity_name)
if not schema:
raise ValueError(f"Unknown document type: {entity_name}")
missing = [a for a in schema["required"] if a not in attrs]
if missing:
raise ValueError(f"Missing required attributes: {missing}")
return {"item": {"attrs": {"attr": [{"name": k, "value": v} for k, v in attrs.items()]},
"resrs": {"res": [{"filename": filename, "base64": file_b64}]},
"acl": {"name": "Public"}, "entityName": entity_name}}
Always specify "Infor Document Management" or "Infor OS IDM" in searches. [src2]Calculate encoded size (original * 1.37) before upload. For near-limit files, use IDM Utilities. [src1]Poll for document availability. Don't immediately search for just-uploaded documents. [src1]Use standard upload API for files > 50 MB. [src1]Always include $offset and $limit. Process in batches of 50-100. [src7]Store returned PID and query by PID directly for confirmation. [src2]# Parse .ionapi file and acquire token
CI=$(jq -r '.ci' credentials.ionapi)
CS=$(jq -r '.cs' credentials.ionapi)
SAAK=$(jq -r '.saak' credentials.ionapi)
SASK=$(jq -r '.sask' credentials.ionapi)
OA=$(jq -r '.oa' credentials.ionapi)
IU=$(jq -r '.iu' credentials.ionapi)
TOKEN=$(curl -s -X POST "$OA" \
-d "grant_type=password&username=$SAAK&password=$SASK&client_id=$CI&client_secret=$CS" \
-H "Content-Type: application/x-www-form-urlencoded" | jq -r '.access_token')
echo "Token length: ${#TOKEN} (should be >100)"
# Test IDM API connectivity
curl -s -o /dev/null -w "%{http_code}" "$IU/IDM/api/items/search" \
-H "Authorization: Bearer $TOKEN" \
-G --data-urlencode "q=/*" --data-urlencode "\$limit=1"
# Check document count for a specific type
curl -s -G "$IU/IDM/api/items/search" \
--data-urlencode "q=/Invoice" --data-urlencode "\$limit=0" \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json" | jq '.totalCount'
# Verify specific document exists
curl -s "$IU/IDM/api/items/${DOCUMENT_PID}" \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
| jq '{pid: .pid, entityName: .entityName, version: .version}'
| Infor OS Release | Date | IDM API Changes | Migration Notes |
|---|---|---|---|
| 2025.x | 2025-Q1 | Large file API threshold configurable | Previously hardcoded at 2 MB [src1] |
| 2024.x | 2024-Q1 | Background upload threshold configurable | Default 20 MB; admin-adjustable [src1] |
| 2023.x | 2023-Q1 | Updated Swagger/OpenAPI docs | API endpoints unchanged [src2] |
| 2022.x | 2022-Q1 | ION Document Flow support | Event-driven document routing [src4] |
| 2021.x | 2021-Q1 | XQuery search improvements | Added $offset/$limit pagination [src7] |
Infor follows a rolling release model for Infor OS. Breaking changes are announced through release notes with one major release cycle (~6-12 months) of advance notice. The IDM REST API does not use formal version numbering. [src2]
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Managing documents within Infor CloudSuite (M3, LN, Syteline) | Need standalone ECM with no Infor dependency | SharePoint, Box, or standalone ECM APIs |
| Integrating document upload/download with Infor ERP workflows | Need to store >2 GB individual files | External blob storage (S3, Azure Blob) with IDM metadata reference |
| Compliance and versioning with check-in/check-out workflow | Need high-throughput ingestion (>1.5M docs/day) | Upgrade to Enterprise tier or use external staging |
| ION-integrated event-driven document routing | Need to create Document Types programmatically | IDM Administration UI (no API for type creation) |
| Attribute-based document search via XQuery | Need full-text content search across document bodies | Elasticsearch or dedicated search platform |