$expand=Result -- GET returns a blank <NEW> record. OData GI feeds are limited to OData v3 with restricted filter operators.$skip/$top) does not work on nested Result collections in the REST approach -- use OData if you need server-side pagination.This card covers exposing Acumatica Generic Inquiries (GIs) as API endpoints. Generic Inquiries are Acumatica's power-user query builder -- they combine data from multiple DACs (Data Access Classes) into custom result sets, similar to SQL views. This card covers two exposure methods: (1) the contract-based REST API approach where you extend a Web Service Endpoint to include GI entities, and (2) the OData v3 approach where you check a box on the GI form to create an OData feed. Both methods work across all Acumatica editions. [src1, src2]
| Property | Value |
|---|---|
| Vendor | Acumatica |
| System | Acumatica ERP 2024 R2 |
| API Surface | REST (Contract-Based) + OData v3 (GI-specific) |
| Current API Version | 24.200.001 |
| Editions Covered | Small Business, Standard, Enterprise |
| Deployment | Cloud (SaaS) and on-premise |
| API Docs | Acumatica Developer Portal |
| Status | GA |
Acumatica provides multiple API surfaces for data access. For Generic Inquiries specifically, two surfaces apply. [src1, src2]
| API Surface | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
| REST Contract-Based (GI via Endpoint Extension) | HTTPS/JSON | Typed entity integrations needing GI data | Limited by endpoint timeout | 50-150 req/min | Yes | No |
| OData v3 (GI Feed) | HTTPS/JSON or XML | BI tools, simple data pulls | Configurable via $top | Shared with REST | Yes | Yes (via pagination) |
| OData v4 (DAC-Based) | HTTPS/JSON | Direct table access without GI | Configurable via $top | Shared with REST | Yes | Yes |
| REST Contract-Based (Standard Entities) | HTTPS/JSON | CRUD on standard Acumatica entities | 1,000 per $batch | 50-150 req/min | Yes | Via $batch |
| SOAP (Legacy) | HTTPS/XML | Legacy integrations | Similar to REST | Shared | Yes | No |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max concurrent API sessions | 3-6 | All API calls | License-tier dependent; L Series Tier 1 = 6 concurrent |
| Session timeout (cookie-based) | ~20 minutes | Cookie auth sessions | Re-authenticate after inactivity |
| Max batch subrequests | 1,000 | REST $batch endpoint | Each subrequest counts separately |
| Default request timeout | ~10 minutes | All API calls | Long GI queries may hit this |
| Max OData $top | No hard limit | OData feeds | Large result sets impact performance |
| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| API requests per minute | 50-150 | Per minute | Base: ~50; L Series Tier 1: ~150; varies by license |
| Concurrent Web Service API requests | 3-6 | Per instance | Queued when exceeded; returns 429 if queue full |
| API users | License-dependent | Per instance | Monitored via SM604000 License Monitoring Console |
[src5]
All Acumatica API access (REST and OData) supports multiple authentication methods. OAuth 2.0 is recommended for production. [src2, src7]
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 (Authorization Code) | Production integrations, third-party apps | Configurable | Yes | Configure via SM303010. Recommended. |
| Cookie-Based Session | Quick testing, internal tools | ~20 min inactivity timeout | No -- re-login | POST to /entity/auth/login |
| Basic Auth (OData only) | Simple OData feeds, BI tools | Per-request | N/A | Username:password in Authorization header |
| Bearer Token (OAuth) | REST and OData after OAuth flow | Varies by config | Yes | Authorization: Bearer {token} |
{} and $expand=Result to execute the GI and retrieve results. [src1, src6]$skip and $top URL parameters cannot be applied to the nested Result array returned by the REST contract-based GI approach. [src3, src6]$filter URL parameters. [src4]START -- User needs to expose Acumatica Generic Inquiry data externally
|
|-- What's the primary use case?
| |-- BI / Reporting tool (Power BI, Excel, Tableau)
| | --> OData GI feed (checkbox on GI form)
| | --> URL: {instance}/OData/{tenant}/{GI_Name}?$format=json
| |
| |-- Structured integration (iPaaS, custom app, typed entities)
| | |-- Need pagination ($skip/$top)?
| | | |-- YES --> OData GI feed (REST doesn't support pagination on Results)
| | | |-- NO --> Contract-Based REST with endpoint extension
| | |
| | --> REST: PUT /entity/{endpoint}/{version}/{GI_Entity}?$expand=Result
| |
| |-- Simple one-off data pull
| | --> OData GI feed with $filter (easiest path)
| |
| |-- Writing data back based on GI results
| --> OData/REST GI for read, standard REST API for write (separate calls)
|
|-- Data volume?
| |-- < 1,000 rows --> Either approach works
| |-- 1,000-10,000 rows --> OData with $top/$skip pagination
| |-- > 10,000 rows --> OData with pagination + batch scheduling
|
|-- Authentication preference?
|-- OAuth 2.0 (recommended) --> Works for both REST and OData
|-- Basic Auth --> OData only
|-- Cookie session --> REST only
| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| Enable GI OData feed | UI | SM208000 > check "Expose via OData" | N/A | Immediate -- no deploy needed |
| Query GI via OData | GET | {instance}/OData/{tenant}/{GI_Name} | N/A | Add ?$format=json for JSON |
| Query GI via OData (24R2+) | GET | {instance}/t/{tenant}/api/odata/gi/{GI_Name} | N/A | New URL format in 2024 R2 |
| Extend endpoint for GI | UI | SM207000 > Extend Endpoint | N/A | Add entity + Results sub-entity + fields |
| Execute GI via REST | PUT | {instance}/entity/{endpoint}/{version}/{GI_Entity}?$expand=Result | {} | Empty body; PUT required, GET returns blank |
| Filter OData GI | GET | ...?$filter={field} eq '{value}' | N/A | OData v3 operators: eq, gt, lt, ge, le, ne |
| Filter REST GI | PUT | Same as Execute | {"FilterField": {"value": "..."}} | Filters in request body, not URL |
| Paginate OData GI | GET | ...?$top=100&$skip=200 | N/A | Works on OData; NOT on REST Results |
| Login (cookie auth) | POST | /entity/auth/login | {"name":"...","password":"...","company":"..."} | Returns session cookie |
| Logout | POST | /entity/auth/logout | N/A | Always logout to free session slot |
Navigate to SM208000 (Generic Inquiry form) and either create a new GI or identify an existing one. The GI defines the data sources (DACs), joins, conditions, and result columns. [src1]
SM208000 -- Generic Inquiry configuration:
1. Design ID: e.g., "InventoryByLocation"
2. Add tables under "Tables" tab
3. Define joins between tables
4. Add result columns under "Results" tab
5. (Optional) Add parameters for runtime filtering
6. Save the Generic Inquiry
Verify: Open the GI from the navigation menu and confirm it returns the expected data.
On the SM208000 form, check the "Expose via OData" checkbox and save. The OData feed is immediately available. [src2]
# OData v3 URL (all versions):
curl -s -u "admin:password" \
"https://yourinstance.acumatica.com/OData/YourTenant/InventoryByLocation?\$format=json&\$top=10"
# OData URL (2024 R2+ new format):
curl -s -u "admin:password" \
"https://yourinstance.acumatica.com/t/YourTenant/api/odata/gi/InventoryByLocation?\$format=json&\$top=10"
Verify: Response contains a value array with GI result rows.
Navigate to SM207000 (Web Service Endpoints) and extend the Default endpoint. Add a new top-level entity mapped to the GI, then create a Results sub-entity. [src1]
SM207000 -- Web Service Endpoint Extension:
1. Select the "Default" endpoint for your version (e.g., 24.200.001)
2. Click "Extend Endpoint" -- name it (e.g., "MyExtEndpoint")
3. Insert new top-level entity: Name = "InventoryByLocation"
4. Under entity, add sub-entity: Name = "Result", ObjectName = "InvByLocation"
5. Under "Result", add fields: ItemID, WarehouseID, LocationID, QtyOnHand
6. Save the endpoint
Verify: Entity appears in endpoint Swagger at {instance}/entity/{endpoint}/{version}/$adoc.
Execute the GI by sending a PUT request with an empty body and $expand=Result. GET will NOT work. [src1, src6]
# Login
curl -s -X POST "https://yourinstance.acumatica.com/entity/auth/login" \
-H "Content-Type: application/json" \
-d '{"name":"admin","password":"yourpassword","company":"YourCompany"}' \
-c cookies.txt
# Execute GI via PUT
curl -s -X PUT \
"https://yourinstance.acumatica.com/entity/MyExtEndpoint/24.200.001/InventoryByLocation?\$expand=Result" \
-H "Content-Type: application/json" \
-b cookies.txt -d '{}'
# Logout
curl -s -X POST "https://yourinstance.acumatica.com/entity/auth/logout" -b cookies.txt
Verify: Response JSON contains "Result": [...] with data rows.
For OData, use $filter URL parameters. For REST, send filter values in the PUT request body. [src4]
# OData filtering -- URL parameters:
curl -s -u "admin:password" \
"https://yourinstance.acumatica.com/OData/YourTenant/InventoryByLocation?\$filter=WarehouseID eq 'WHOLESALE'&\$top=50&\$format=json"
# REST filtering -- values in request body:
curl -s -X PUT \
"https://yourinstance.acumatica.com/entity/MyExtEndpoint/24.200.001/InventoryByLocation?\$expand=Result" \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"WarehouseID": {"value": "WHOLESALE"}}'
Verify: Returned results are filtered to matching records only.
# Input: Acumatica instance URL, OAuth access token, GI name
# Output: JSON result rows from the Generic Inquiry
import requests
instance_url = "https://yourinstance.acumatica.com"
access_token = "YOUR_OAUTH_ACCESS_TOKEN"
gi_name = "InventoryByLocation"
tenant = "YourTenant"
odata_url = f"{instance_url}/t/{tenant}/api/odata/gi/{gi_name}"
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json"
}
params = {
"$format": "json",
"$top": 100,
"$filter": "WarehouseID eq 'WHOLESALE'"
}
response = requests.get(odata_url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
for row in data.get("value", []):
print(f"Item: {row['ItemID']}, Warehouse: {row['WarehouseID']}")
// Input: Acumatica instance URL, credentials, endpoint name
// Output: GI result rows from the contract-based REST endpoint
const fetch = require("node-fetch"); // npm install node-fetch@2
const instanceUrl = "https://yourinstance.acumatica.com";
const endpoint = "MyExtEndpoint";
const version = "24.200.001";
async function queryGI() {
// Login
const loginResp = await fetch(`${instanceUrl}/entity/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "admin", password: "pass", company: "Co" }),
redirect: "manual"
});
const cookies = loginResp.headers.raw()["set-cookie"];
const cookieHeader = cookies.map(c => c.split(";")[0]).join("; ");
// Execute GI via PUT (NOT GET)
const giResp = await fetch(
`${instanceUrl}/entity/${endpoint}/${version}/InventoryByLocation?$expand=Result`,
{ method: "PUT", headers: { "Content-Type": "application/json", Cookie: cookieHeader }, body: "{}" }
);
const data = await giResp.json();
data.Result?.forEach(row =>
console.log(`Item: ${row.ItemID?.value}, Qty: ${row.QtyOnHand?.value}`)
);
// Logout
await fetch(`${instanceUrl}/entity/auth/logout`, { method: "POST", headers: { Cookie: cookieHeader } });
}
queryGI().catch(console.error);
# Input: Acumatica instance URL, credentials, GI name
# Output: JSON result set from Generic Inquiry
curl -s -u "admin:password" \
"https://yourinstance.acumatica.com/OData/YourTenant/InventoryByLocation?\$format=json&\$top=5" \
-H "Accept: application/json" | jq .
# Expected output:
# {
# "value": [
# {"ItemID": "ITEM001", "WarehouseID": "WHOLESALE", "QtyOnHand": 150},
# ...
# ]
# }
| GI Result Column | REST Response Field | OData Response Field | Type | Gotcha |
|---|---|---|---|---|
| Inventory ID | ItemID.value | ItemID | String | REST wraps in {value: "..."}; OData returns plain value |
| Warehouse | WarehouseID.value | WarehouseID | String | API name matches GI column alias, not DAC field name |
| Quantity On Hand | QtyOnHand.value | QtyOnHand | Decimal | REST returns as string; OData returns as number |
| Last Modified Date | LastModified.value | LastModified | DateTime | REST: ISO 8601; OData v3: /Date(timestamp)/ |
| Boolean/Checkbox | IsActive.value | IsActive | Boolean | OData filter: use lowercase true/false or 0/1 |
| Custom field | UsrCustomField.value | UsrCustomField | Varies | Custom fields require USR prefix |
{value: "..."} object: Unlike OData which returns plain values, the Acumatica contract-based REST API wraps every field. Your deserialization must unwrap this layer. [src1, src7]/Date()/ notation: OData v3 GI feeds return dates as /Date(1234567890000)/ (milliseconds since epoch), not ISO 8601. [src2]$filter=IsActive eq true (not TRUE). Alternatively use 1 or 0. [src4]| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 | Unauthorized | Invalid credentials, expired token, or session timeout | Re-authenticate; check OAuth token expiry |
| 403 | Forbidden | User lacks access to GI or underlying DACs | Grant GI access rights and entity-level permissions |
| 404 | Not Found | Wrong endpoint name, version, or GI entity name | Verify endpoint includes GI entity; check URL and version |
| 429 | Too Many Requests | Exceeded per-minute API limit or concurrent session cap | Implement backoff; check SM604000 for limits |
| 500 | Internal Server Error | BQL query error, timeout, or missing DAC permissions | Check GI runs manually in UI; review trace logs |
| <NEW> record | Used GET instead of PUT | GET on GI entity returns default/blank record by design | Switch to PUT with empty body and $expand=Result |
| Empty Result | No data or misconfigured | Results sub-entity not mapped or GI conditions exclude all records | Verify GI returns data in UI; check endpoint mapping |
Reuse session cookies and call /entity/auth/logout when done. Use OAuth 2.0 bearer tokens. [src5, src7]Add GI parameters to pre-filter server-side. Use OData $top/$skip pagination. [src1]Store endpoint version in config. Re-extend endpoint for new version after upgrade. [src1, src7]Version-control GI definitions via customization projects. Test OData output after any GI modification. [src2]$filter=IsActive eq TRUE (uppercase) on OData v3 fails. Fix: Use lowercase: $filter=IsActive eq true, or numeric: $filter=IsActive eq 1. [src4]# BAD -- GET returns a blank <NEW> default record, not the GI results
curl -s -X GET \
"https://instance.acumatica.com/entity/MyEndpoint/24.200.001/InventoryByLocation?$expand=Result" \
-b cookies.txt
# Returns: {"id": "...", "Result": [], ...} -- empty results
# GOOD -- PUT triggers the GI execution and returns results
curl -s -X PUT \
"https://instance.acumatica.com/entity/MyEndpoint/24.200.001/InventoryByLocation?$expand=Result" \
-H "Content-Type: application/json" -b cookies.txt -d '{}'
# Returns: {"id": "...", "Result": [{"ItemID": {"value": "ITEM001"}, ...}]}
# BAD -- creates new session per request, exhausts concurrent session limit
for item_id in item_ids:
session = requests.Session()
session.post(f"{url}/entity/auth/login", json=creds) # new session each time
result = session.put(f"{url}/entity/endpoint/ver/GI?$expand=Result",
json={"ItemID": {"value": item_id}})
# Never logs out -- session slot consumed until timeout
# GOOD -- single session, reused, properly closed
session = requests.Session()
session.post(f"{url}/entity/auth/login", json=creds)
for item_id in item_ids:
result = session.put(f"{url}/entity/endpoint/ver/GI?$expand=Result",
json={"ItemID": {"value": item_id}})
process(result.json())
session.post(f"{url}/entity/auth/logout") # free the session slot
# BAD -- $skip/$top do not work on nested Result collections
curl -s -X PUT \
"https://instance.acumatica.com/entity/endpoint/ver/GI?$expand=Result&$skip=100&$top=50" \
-H "Content-Type: application/json" -b cookies.txt -d '{}'
# $skip is ignored on nested Results -- returns all rows
# GOOD -- OData supports $skip/$top natively on GI feeds
curl -s -u "admin:password" \
"https://instance.acumatica.com/OData/Tenant/InventoryByLocation?\$top=50&\$skip=100&\$format=json"
# Returns rows 101-150 as expected
$expand=Result: Without this parameter, the PUT returns only top-level entity fields, not GI data rows. Fix: Always append ?$expand=Result to the REST GI URL. [src1]Create hierarchy: TopEntity > Result (sub-entity) > Fields. [src1]Use a distinct name like "InvByLocation" for the Results ObjectName. [src1]Open SM208000, check the box, save. No deployment needed. [src2]Use: $filter=LastModified gt datetime'2024-01-01T00:00:00'. [src4]Reduce concurrent sessions, implement proper logout, or use OAuth 2.0. [src5]# Check if GI OData feed is accessible (Basic Auth)
curl -s -o /dev/null -w "%{http_code}" -u "admin:password" \
"https://yourinstance.acumatica.com/OData/YourTenant/YourGIName?\$format=json&\$top=1"
# Expected: 200
# List available OData GI feeds (metadata)
curl -s -u "admin:password" \
"https://yourinstance.acumatica.com/OData/YourTenant/\$metadata" \
-H "Accept: application/xml"
# Test REST authentication (cookie-based)
curl -s -X POST "https://yourinstance.acumatica.com/entity/auth/login" \
-H "Content-Type: application/json" \
-d '{"name":"admin","password":"password","company":"Company"}' \
-c cookies.txt -o /dev/null -w "%{http_code}"
# Expected: 204
# Execute GI and count result rows
curl -s -X PUT \
"https://yourinstance.acumatica.com/entity/MyEndpoint/24.200.001/InventoryByLocation?\$expand=Result" \
-H "Content-Type: application/json" -b cookies.txt -d '{}' \
| jq '.Result | length'
# Check API license limits: navigate to SM604000 in Acumatica UI
| API Version | Release Date | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| 24.200.001 (2024 R2) | 2024-10 | Current | New OData GI URL format | Old URL still works; new format recommended |
| 23.200.001 (2023 R2) | 2023-10 | Supported | OData GI parameter filtering added | GIs with parameters can now be filtered via OData |
| 23.100.001 (2023 R1) | 2023-04 | Supported | OAuth 2.0 connected applications | New auth flow; cookie-based still supported |
| 22.200.001 (2022 R2) | 2022-10 | Supported | None for GI APIs | -- |
| 18.200.001 (2019 R1) | 2019-01 | EOL | Contract-Based API GI support introduced | Original version for GI endpoint extension |
Acumatica supports API endpoints for the current and previous two major releases (approximately 3 years). Older endpoint versions may stop working after an upgrade. Extended endpoints must be re-created or migrated when upgrading to a new major version. [src1, src7]
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Need custom cross-table query results already defined as a GI | Need CRUD operations (create/update/delete) | Standard Acumatica REST contract-based API |
| Connecting BI tools (Power BI, Excel) to Acumatica data | Need real-time webhooks or push notifications | Acumatica Push Notifications (SM302000) or Webhooks (SM304000) |
| Want power users to define queries without developer involvement | Query needs OData v4 features ($apply, $compute) | OData v4 DAC-based access |
| Result set is under 10,000 rows | Need to extract millions of records for data warehouse | Direct database replication or data export |
| Need pagination on large result sets | Need pagination via REST (only works on OData) | OData GI feed with $top/$skip |
$apply, $compute, and substringof() may not work. Test before relying on them in production. [src2]