/api/v2/odata/{Company}/BaqSvc/{BaqName}/Data (OData) or /api/v2/baq/{BaqName} (REST). Configure Access Scopes and API keys to control external access. [src1, src2]$top and $skip for pagination. Cloud/SaaS users cannot override this via web.config. [src3, src8]$filter applies post-processing — use BAQ parameters for large datasets to avoid memory issues. [src7]TokenResource.svc (8-hour lifetime). [src1, src6]This card covers how to expose Epicor Business Activity Queries (BAQs) as external REST and OData API endpoints using Epicor Kinetic REST Services v2. BAQs are custom queries built in the Epicor BAQ Designer that retrieve specific data from the Epicor database — they inherit all table and field security from the Epicor security model. This card applies to both cloud/SaaS and on-premise Epicor Kinetic deployments, though certain configuration options (like web.config modifications) are only available on-premise. [src1, src4]
| Property | Value |
|---|---|
| Vendor | Epicor |
| System | Epicor Kinetic (ERP) — REST API v2 |
| API Surface | REST + OData v4 |
| Current API Version | REST v2 (Kinetic 2022.2+) |
| Editions Covered | Standard, Enterprise, Cloud/SaaS, On-Premise |
| Deployment | Cloud / On-Premise / Hybrid |
| API Docs | Swagger UI (per-instance) |
| Status | GA |
Epicor exposes BAQ data through two primary endpoint patterns: the OData endpoint (optimized for data feeds and BI tools) and the REST endpoint (optimized for programmatic CRUD operations on updatable BAQs). Both share the same authentication infrastructure. [src1, src2, src5]
| API Surface | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
OData BAQ (/odata/{Co}/BaqSvc/{BAQ}/Data) | HTTPS/JSON (OData v4) | BI tools, Excel, Power BI, Grafana feeds | 100 default ($top/$skip for more) | No hard limit; server-dependent | Yes | Via pagination |
REST BAQ (/baq/{BAQ}) | HTTPS/JSON | Programmatic access, updatable BAQ CRUD | 100 default (configurable on-prem) | No hard limit; server-dependent | Yes | No |
REST BAQ GetNew (/baq/{BAQ}/GetNew) | HTTPS/JSON | Get blank row template for updatable BAQ inserts | 1 row template | N/A | Yes | No |
REST BAQSvc (Erp.BO.BAQSvc) | HTTPS/JSON | SSRS report submission, advanced BAQ operations | Varies | N/A | Yes | No |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Default max row count | 100 | All BAQ REST/OData endpoints | Use $top and $skip to paginate beyond default [src8] |
| Max file download size | 2 GB | ServerFileDownload endpoint | System.IO limitation [src3] |
| Max allowed content length | ~4 GB | All REST endpoints | Configurable via MaxAllowedContentLength in web.config (on-prem only) [src3] |
| Max batch size (updatable) | 1 row per PATCH | Updatable BAQ updates | Pass one changed row at a time to the Data method [src5] |
| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| API call limit | No explicit per-day limit | N/A | Epicor does not publish formal API rate limits — server resources are the practical constraint [src3] |
| Concurrent sessions | Tied to Epicor license count | Per-instance | Each API session consumes a CAL (Client Access License) [src5] |
| DefaultMaxRowCount override | Configurable (0 = unlimited) | Per-instance | On-premise only; cloud/SaaS cannot modify [src3] |
Epicor REST API supports three authentication methods. All three work for BAQ endpoints. API key authentication is the recommended approach for service-to-service integrations. [src1, src6]
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| API Key | Service integrations, external apps, BI tools | No expiry (until rotated) | N/A | Header: X-API-Key or query parameter ?api-key={key}. Created in API Key Maintenance. |
| Basic Authentication | Quick testing, internal tools | Session-based | N/A | Authorization: Basic {base64(user:pass)}. Each request creates a new session. |
| Bearer Token | Long-running sessions, server-to-server | 8 hours (28,800s) | No — acquire new token | POST to TokenResource.svc with username/password headers. Returns JWT. |
Erp.BO.BAQSvc in the services list AND add the specific BAQ name to the BAQs list. Without this, the API returns 403. [src1]X-API-Key header on subsequent requests. [src6]DefaultMaxRowCount — the 100-row default per request is effectively fixed; pagination via $top/$skip is mandatory for larger result sets. [src3]$filter is post-processing, not database-level — filtering happens after the BAQ query executes, meaning the full result set is loaded into memory first. For large BAQs, use BAQ parameters instead. [src7]?api-key=, forcing fallback to Basic Auth. [src2]/api/help/v1/ and v2 uses /api/help/v2/. Some older documentation references v1 endpoints which still work but lack OData query parameter support. [src4]START — User needs to expose Epicor BAQ as API endpoint
|-- What's the use case?
| |-- Read-only data extraction (reporting, dashboards)
| | |-- BI tool (Excel, Power BI, Grafana)?
| | | |-- YES --> OData endpoint: /api/v2/odata/{Co}/BaqSvc/{BAQ}/Data
| | | | |-- Excel --> Use Basic Auth (Excel strips API key from URL) [src2]
| | | | |-- Power BI --> Use API key in header or Basic Auth
| | | | |-- Grafana --> Use API key as query parameter [src2]
| | | |-- NO --> REST endpoint: /api/v2/baq/{BAQ}
| | |-- Need filtering?
| | |-- Small dataset (<10K rows) --> OData $filter (post-processing) [src7]
| | |-- Large dataset (>10K rows) --> BAQ parameters (DB-level) [src7]
| |-- Updatable BAQ (CRUD operations)
| | |-- Create new record --> POST to /api/v2/baq/{BAQ}/GetNew, then PATCH with data [src5]
| | |-- Update existing record --> PATCH to /api/v2/baq/{BAQ}/Data [src5]
| | |-- Delete record --> DELETE to /api/v2/baq/{BAQ}/Data
| |-- SSRS report generation via BAQ
| --> POST to Ice.RPT.BAQReportSvc/SubmitToAgent [src5]
|-- Which authentication?
| |-- Service-to-service --> API key (no session overhead) [src1]
| |-- User-context --> Basic Auth or Bearer token [src6]
| |-- Long-running process --> Bearer token (8h lifetime) [src6]
|-- Pagination needed?
|-- < 100 rows --> No pagination needed
|-- > 100 rows --> Use $top and $skip [src8]
| Operation | Method | Endpoint | Auth Header | Notes |
|---|---|---|---|---|
| Execute BAQ (OData) | GET | /api/v2/odata/{Company}/BaqSvc/{BaqName}/Data | X-API-Key: {key} | Returns OData-formatted JSON |
| Execute BAQ (REST) | GET | /api/v2/baq/{BaqName} | X-API-Key: {key} | Returns standard JSON |
| Execute BAQ with params | GET | .../Data?Param1=Value1 | X-API-Key: {key} | BAQ parameters as query params |
| Filter results (OData) | GET | .../Data?$filter=Field eq 'Value' | X-API-Key: {key} | Post-processing filter |
| Paginate results | GET | .../Data?$top=100&$skip=200 | X-API-Key: {key} | Third page of 100 rows |
| Select fields | GET | .../Data?$select=Field1,Field2 | X-API-Key: {key} | Reduce payload size |
| Get blank row (updatable) | GET | /api/v2/baq/{BaqName}/GetNew | X-API-Key: {key} | Returns template row for insert |
| Update row (updatable) | PATCH | /api/v2/baq/{BaqName}/Data | X-API-Key: {key} | Pass single row as {"ds": {...}} |
| Get Bearer token | POST | /{instance}/TokenResource.svc | username + password headers | Returns JWT valid for 8 hours |
| Access Swagger help | GET | /api/help/v2/ | Browser session | Interactive API docs |
Create your Business Activity Query in the Epicor Kinetic UI. Define the tables, joins, calculated fields, and any parameters you need. Mark the BAQ as "shared" if external applications need to access it. [src1, src4]
Epicor Kinetic steps:
1. Open BAQ Designer (System Management > Business Activity Queries)
2. Create new BAQ or open existing
3. Define query tables, fields, joins, and criteria
4. For parameterized BAQs: add parameters on the Parameters tab
5. For updatable BAQs: enable "Updatable" flag on the General tab
6. Test with "Test" button
7. Save — note the BAQ ID (e.g., "MyCustomBAQ")
Verify: Run the BAQ in the designer and confirm it returns expected results.
Access Scopes control which services and BAQs are available to API consumers. Navigate to System Setup > Security Maintenance > Access Scope Maintenance. [src1]
Access Scope setup:
1. Navigate to System Setup > Security Maintenance > Access Scope Maintenance
2. Create a new scope with an ID (e.g., "ExternalBAQAccess")
3. In the Services list, confirm "Erp.BO.BAQSvc" is listed
4. In the BAQs list, add your specific BAQ name (e.g., "MyCustomBAQ")
5. Save the Access Scope
Verify: The Access Scope shows Erp.BO.BAQSvc in services and your BAQ name in the BAQs list.
Create an API key that uses the Access Scope from step 2. [src1]
API Key setup:
1. Navigate to System Setup > Security Maintenance > API Key Maintenance
2. Create a new API Key
3. Fill in required fields (description, user, company)
4. Select the Access Scope created in step 2
5. Save — IMMEDIATELY copy the API key value
6. Store the key in a secure external location
WARNING: The API key value is shown only once. If lost, you must create a new key.
Verify: The API key appears in the API Key Maintenance list with the correct Access Scope.
Use the OData or REST endpoint to retrieve BAQ data. Include the API key in the header or as a query parameter. [src1, src2]
# OData endpoint (recommended for data feeds)
curl -s "https://your-server/instance/api/v2/odata/Company/BaqSvc/MyCustomBAQ/Data" \
-H "X-API-Key: your-api-key-here" \
-H "Accept: application/json"
# REST endpoint (alternative)
curl -s "https://your-server/instance/api/v2/baq/MyCustomBAQ" \
-H "X-API-Key: your-api-key-here" \
-H "Accept: application/json"
# With BAQ parameter
curl -s "https://your-server/instance/api/v2/odata/Company/BaqSvc/MyCustomBAQ/Data?CustomerID=ACME001" \
-H "X-API-Key: your-api-key-here" \
-H "Accept: application/json"
Verify: Response returns JSON with a value array containing BAQ result rows. HTTP status 200.
The default max row count is 100. Use $top and $skip OData parameters to paginate through larger result sets. [src8]
# Page 1 (rows 0-99)
curl -s ".../BaqSvc/MyCustomBAQ/Data?$top=100&$skip=0" \
-H "X-API-Key: your-api-key-here"
# Page 2 (rows 100-199)
curl -s ".../BaqSvc/MyCustomBAQ/Data?$top=100&$skip=100" \
-H "X-API-Key: your-api-key-here"
Verify: Each page returns up to 100 rows. When fewer rows than $top are returned, you have reached the end.
Use OData query parameters to filter results and select specific fields. Remember that $filter is post-processing — for better performance, use BAQ parameters instead. [src7]
# Filter with equality
curl -s ".../Data?$filter=PartPlant_PersonID eq 'JSMITH'" \
-H "X-API-Key: your-api-key-here"
# Select specific fields only
curl -s ".../Data?$select=OrderNum,CustomerName,OrderTotal" \
-H "X-API-Key: your-api-key-here"
# Combine filter, select, and pagination
curl -s ".../Data?$filter=OrderTotal gt 1000&$select=OrderNum,CustomerName&$top=50" \
-H "X-API-Key: your-api-key-here"
Verify: Response contains only matching rows and selected fields.
# Input: Epicor server URL, company, BAQ name, API key
# Output: All BAQ results (paginated)
import requests
SERVER = "https://your-epicor-server/YourInstance"
COMPANY = "YourCompany"
BAQ_NAME = "MyCustomBAQ"
API_KEY = "your-api-key-here"
PAGE_SIZE = 100
def get_baq_data(baq_name, params=None, page_size=PAGE_SIZE):
"""Fetch all rows from an Epicor BAQ with automatic pagination."""
url = f"{SERVER}/api/v2/odata/{COMPANY}/BaqSvc/{baq_name}/Data"
headers = {
"X-API-Key": API_KEY,
"Accept": "application/json"
}
all_rows = []
skip = 0
while True:
query_params = {"$top": page_size, "$skip": skip}
if params:
query_params.update(params)
resp = requests.get(url, headers=headers, params=query_params)
resp.raise_for_status()
data = resp.json()
rows = data.get("value", [])
all_rows.extend(rows)
if len(rows) < page_size:
break
skip += page_size
return all_rows
# Usage
results = get_baq_data(BAQ_NAME)
print(f"Total rows: {len(results)}")
# With BAQ parameter
results = get_baq_data(BAQ_NAME, params={"CustomerID": "ACME001"})
// Input: Epicor server URL, company, BAQ name, API key
// Output: BAQ results as JSON
const fetch = require("node-fetch"); // npm install node-fetch@2
const SERVER = "https://your-epicor-server/YourInstance";
const COMPANY = "YourCompany";
const API_KEY = "your-api-key-here";
async function getBAQData(baqName, params = {}) {
const url = new URL(
`${SERVER}/api/v2/odata/${COMPANY}/BaqSvc/${baqName}/Data`
);
Object.entries(params).forEach(([key, val]) => url.searchParams.set(key, val));
const response = await fetch(url.toString(), {
headers: {
"X-API-Key": API_KEY,
"Accept": "application/json",
},
});
if (!response.ok) {
throw new Error(`BAQ request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.value || [];
}
(async () => {
const rows = await getBAQData("MyCustomBAQ", {
"$top": "100",
"$filter": "OrderTotal gt 1000",
});
console.log(`Rows returned: ${rows.length}`);
})();
# === Method 1: API Key (recommended) ===
curl -s "https://server/instance/api/v2/odata/Company/BaqSvc/MyBAQ/Data" \
-H "X-API-Key: your-api-key" \
-H "Accept: application/json" | jq .
# === Method 2: Basic Authentication ===
curl -s "https://server/instance/api/v2/odata/Company/BaqSvc/MyBAQ/Data" \
-u "username:password" \
-H "Accept: application/json" | jq .
# === Method 3: Bearer Token ===
TOKEN=$(curl -s -X POST "https://server/instance/TokenResource.svc" \
-H "username: epicor_user" \
-H "password: epicor_pass" \
-H "Accept: application/json" | jq -r '.AccessToken')
curl -s "https://server/instance/api/v2/odata/Company/BaqSvc/MyBAQ/Data" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: your-api-key" \
-H "Accept: application/json" | jq .
| Component | Value | Example | Notes |
|---|---|---|---|
| Base URL | https://{server}/{instance} | https://centralusdtapp01.epicorsaas.com/saas512 | Varies by deployment |
| OData BAQ path | /api/v2/odata/{Company}/BaqSvc/{BaqName}/Data | /api/v2/odata/MYCO/BaqSvc/SalesOrders/Data | Recommended for data feeds |
| REST BAQ path | /api/v2/baq/{BaqName} | /api/v2/baq/SalesOrders | Alternative format |
| REST v1 path | /api/v1/BaqSvc/{BaqName} | /api/v1/BaqSvc/SalesOrders | Legacy, still functional |
| Swagger help | /api/help/v2/ | /api/help/v2/ | Interactive docs |
| Token endpoint | /{instance}/TokenResource.svc | /saas512/TokenResource.svc | Bearer token acquisition |
OrderHed_OrderNum), which differs from display names. [src2]Calculated_ prefix: Any calculated columns in the BAQ appear with a Calculated_ prefix in API results. [src2]| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 Unauthorized | Invalid or missing authentication | API key not provided, expired, or invalid | Verify API key exists and is correctly copied; check Access Scope includes the BAQ |
| 403 Forbidden | Access denied | Access Scope does not include Erp.BO.BAQSvc or the specific BAQ | Add Erp.BO.BAQSvc to Access Scope services AND add BAQ name to BAQs list |
| 404 Not Found | BAQ or endpoint not found | BAQ name misspelled, not shared, or wrong company ID | Verify BAQ name in BAQ Designer; check company ID; use Swagger help to confirm |
| 500 Internal Server Error | Server-side error | BAQ query has SQL errors, timeout, or references deleted objects | Test BAQ in designer; check Epicor server logs; simplify query |
| Root element is missing | Malformed JSON payload | Smart quotes or encoding issues in request body | Use straight quotes in JSON; verify UTF-8 encoding without BOM |
Document all API keys in a secrets manager with ownership and rotation schedule. Monitor for 401 errors. [src1]Version your BAQs (e.g., SalesOrders_v1, SalesOrders_v2). Never modify a BAQ that has external consumers — create a new version. [src3]Always add BAQ parameters for large tables. Use $top to limit initial requests. [src3, src7]Use API key authentication (shares sessions). Use requests.Session() in Python to reuse sessions. [src5]Monitor Epicor release notes and SaaS infrastructure announcements. Test API endpoints after each update window. [src3]# BAD — $filter is post-processing; the full BAQ executes first, loading
# millions of rows into memory, then filtering client-side
url = f"{SERVER}/api/v2/odata/{COMPANY}/BaqSvc/AllOrders/Data"
url += "?$filter=CustomerID eq 'ACME001'"
# This loads ALL orders into memory, then filters — slow and dangerous
# GOOD — BAQ parameters filter at the DB level before results are loaded
url = f"{SERVER}/api/v2/odata/{COMPANY}/BaqSvc/OrdersByCustomer/Data"
url += "?CustomerID=ACME001"
# This adds a WHERE clause to the SQL, returning only matching rows
# BAD — assumes result fits in default 100 rows; silently truncates data
url = f"{SERVER}/api/v2/odata/{COMPANY}/BaqSvc/AllParts/Data"
resp = requests.get(url, headers=headers)
parts = resp.json()["value"] # Only first 100 rows!
# GOOD — fetches all pages regardless of result size
all_parts = []
skip = 0
while True:
resp = requests.get(f"{url}?$top=100&$skip={skip}", headers=headers)
rows = resp.json().get("value", [])
all_parts.extend(rows)
if len(rows) < 100:
break
skip += 100
# BAD — An Epicor admin adds a new column to "SalesOrders" BAQ.
# All Power BI reports and external integrations using field positions break.
# Renamed fields cause KeyError / undefined errors in consuming applications.
# GOOD — Create "SalesOrders_v2" with the new column.
# Migrate consumers one at a time from v1 to v2.
# Keep v1 running until all consumers are migrated.
# Only then deprecate SalesOrders_v1.
Erp.BO.BAQSvc is necessary but not sufficient — you must also add each BAQ name to the scope's BAQs list. Fix: Open Access Scope Maintenance, navigate to the BAQs tab, and add each BAQ. [src1]Always use v2 endpoints (/api/v2/odata/...) for OData query parameter support. [src4]?api-key=. Fix: Use Basic Authentication when connecting Excel, or use Power Query with a custom connector that sets the X-API-Key header. [src2]$filter values with spaces or special characters must be URL-encoded. Fix: Use URL encoding libraries (urllib.parse.quote in Python, encodeURIComponent in JavaScript). [src7]Table_Column format, not display labels. Fix: Use the Swagger help page or a test API call to identify exact field names. [src2]Use API key authentication for automated integrations. [src5]# Test API key authentication against a BAQ
curl -s -o /dev/null -w "%{http_code}" \
"https://server/instance/api/v2/odata/Company/BaqSvc/MyBAQ/Data?$top=1" \
-H "X-API-Key: your-api-key"
# Expected: 200 (success), 401 (bad key), 403 (bad scope), 404 (bad BAQ name)
# Test Bearer token acquisition
curl -s -X POST "https://server/instance/TokenResource.svc" \
-H "username: epicor_user" \
-H "password: epicor_pass" \
-H "Accept: application/json" | jq '{token_type: .TokenType, expires_in: .ExpiresIn}'
# Expected: {"token_type": "Bearer", "expires_in": 28800}
# Check BAQ response structure (first row only)
curl -s "https://server/instance/api/v2/odata/Company/BaqSvc/MyBAQ/Data?$top=1" \
-H "X-API-Key: your-api-key" | jq '.value[0] | keys'
# Count total BAQ rows
curl -s "https://server/instance/api/v2/odata/Company/BaqSvc/MyBAQ/Data?$count=true&$top=0" \
-H "X-API-Key: your-api-key" | jq '.["@odata.count"]'
| API Version | Release Date | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| REST v2 (Kinetic 2022.2+) | 2022-09 | Current | OData query parameter support added; new URL pattern | Use /api/v2/odata/ for new integrations |
| REST v2 (Kinetic 2024.1) | 2024-03 | Current | Token endpoint standardized | TokenResource.svc endpoint for bearer tokens |
| REST v1 | 2016 | Supported (legacy) | Limited OData support | Use /api/v1/BaqSvc/ format; migration to v2 recommended |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Extracting data for dashboards, reports, or BI tools | Building complex business logic that modifies multiple related tables | Epicor Business Objects (BO) REST API or EFx Functions |
| Providing read-only data feeds to external applications | Orchestrating multi-step transactions | Epicor BPM / EFx Functions with proper transaction handling |
| Connecting Excel or Power BI to live Epicor data | Need real-time push notifications when data changes | Epicor Service Connect / BPM Event-Driven patterns |
| Simple CRUD on single-table updatable BAQs | Bulk data migration (>100K records) | Epicor DMT (Data Migration Tool) or direct database ETL |