Epicor BAQ API Endpoints: Exposing Business Activity Queries via REST/OData

Type: ERP Integration System: Epicor Kinetic (REST API v2) Confidence: 0.88 Sources: 8 Verified: 2026-03-02 Freshness: 2026-03-02

TL;DR

System Profile

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]

PropertyValue
VendorEpicor
SystemEpicor Kinetic (ERP) — REST API v2
API SurfaceREST + OData v4
Current API VersionREST v2 (Kinetic 2022.2+)
Editions CoveredStandard, Enterprise, Cloud/SaaS, On-Premise
DeploymentCloud / On-Premise / Hybrid
API DocsSwagger UI (per-instance)
StatusGA

API Surfaces & Capabilities

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 SurfaceProtocolBest ForMax Records/RequestRate LimitReal-time?Bulk?
OData BAQ (/odata/{Co}/BaqSvc/{BAQ}/Data)HTTPS/JSON (OData v4)BI tools, Excel, Power BI, Grafana feeds100 default ($top/$skip for more)No hard limit; server-dependentYesVia pagination
REST BAQ (/baq/{BAQ})HTTPS/JSONProgrammatic access, updatable BAQ CRUD100 default (configurable on-prem)No hard limit; server-dependentYesNo
REST BAQ GetNew (/baq/{BAQ}/GetNew)HTTPS/JSONGet blank row template for updatable BAQ inserts1 row templateN/AYesNo
REST BAQSvc (Erp.BO.BAQSvc)HTTPS/JSONSSRS report submission, advanced BAQ operationsVariesN/AYesNo

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueApplies ToNotes
Default max row count100All BAQ REST/OData endpointsUse $top and $skip to paginate beyond default [src8]
Max file download size2 GBServerFileDownload endpointSystem.IO limitation [src3]
Max allowed content length~4 GBAll REST endpointsConfigurable via MaxAllowedContentLength in web.config (on-prem only) [src3]
Max batch size (updatable)1 row per PATCHUpdatable BAQ updatesPass one changed row at a time to the Data method [src5]

Rolling / Daily Limits

Limit TypeValueWindowEdition Differences
API call limitNo explicit per-day limitN/AEpicor does not publish formal API rate limits — server resources are the practical constraint [src3]
Concurrent sessionsTied to Epicor license countPer-instanceEach API session consumes a CAL (Client Access License) [src5]
DefaultMaxRowCount overrideConfigurable (0 = unlimited)Per-instanceOn-premise only; cloud/SaaS cannot modify [src3]

Authentication

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]

FlowUse WhenToken LifetimeRefresh?Notes
API KeyService integrations, external apps, BI toolsNo expiry (until rotated)N/AHeader: X-API-Key or query parameter ?api-key={key}. Created in API Key Maintenance.
Basic AuthenticationQuick testing, internal toolsSession-basedN/AAuthorization: Basic {base64(user:pass)}. Each request creates a new session.
Bearer TokenLong-running sessions, server-to-server8 hours (28,800s)No — acquire new tokenPOST to TokenResource.svc with username/password headers. Returns JWT.

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

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]

Quick Reference

OperationMethodEndpointAuth HeaderNotes
Execute BAQ (OData)GET/api/v2/odata/{Company}/BaqSvc/{BaqName}/DataX-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 paramsGET.../Data?Param1=Value1X-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 resultsGET.../Data?$top=100&$skip=200X-API-Key: {key}Third page of 100 rows
Select fieldsGET.../Data?$select=Field1,Field2X-API-Key: {key}Reduce payload size
Get blank row (updatable)GET/api/v2/baq/{BaqName}/GetNewX-API-Key: {key}Returns template row for insert
Update row (updatable)PATCH/api/v2/baq/{BaqName}/DataX-API-Key: {key}Pass single row as {"ds": {...}}
Get Bearer tokenPOST/{instance}/TokenResource.svcusername + password headersReturns JWT valid for 8 hours
Access Swagger helpGET/api/help/v2/Browser sessionInteractive API docs

Step-by-Step Integration Guide

1. Build and save the BAQ in Epicor BAQ Designer

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.

2. Create an Access Scope for the BAQ

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.

3. Generate an API key bound to the Access Scope

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.

4. Call the BAQ endpoint with authentication

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.

5. Implement pagination for large result sets

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.

6. Apply OData filters and field selection

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.

Code Examples

Python: Execute BAQ with pagination

# 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"})

JavaScript/Node.js: Execute BAQ with API key

// 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}`);
})();

cURL: Quick BAQ API test with all auth methods

# === 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 .

Data Mapping

BAQ Endpoint URL Pattern Reference

ComponentValueExampleNotes
Base URLhttps://{server}/{instance}https://centralusdtapp01.epicorsaas.com/saas512Varies by deployment
OData BAQ path/api/v2/odata/{Company}/BaqSvc/{BaqName}/Data/api/v2/odata/MYCO/BaqSvc/SalesOrders/DataRecommended for data feeds
REST BAQ path/api/v2/baq/{BaqName}/api/v2/baq/SalesOrdersAlternative format
REST v1 path/api/v1/BaqSvc/{BaqName}/api/v1/BaqSvc/SalesOrdersLegacy, still functional
Swagger help/api/help/v2//api/help/v2/Interactive docs
Token endpoint/{instance}/TokenResource.svc/saas512/TokenResource.svcBearer token acquisition

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

CodeMeaningCauseResolution
401 UnauthorizedInvalid or missing authenticationAPI key not provided, expired, or invalidVerify API key exists and is correctly copied; check Access Scope includes the BAQ
403 ForbiddenAccess deniedAccess Scope does not include Erp.BO.BAQSvc or the specific BAQAdd Erp.BO.BAQSvc to Access Scope services AND add BAQ name to BAQs list
404 Not FoundBAQ or endpoint not foundBAQ name misspelled, not shared, or wrong company IDVerify BAQ name in BAQ Designer; check company ID; use Swagger help to confirm
500 Internal Server ErrorServer-side errorBAQ query has SQL errors, timeout, or references deleted objectsTest BAQ in designer; check Epicor server logs; simplify query
Root element is missingMalformed JSON payloadSmart quotes or encoding issues in request bodyUse straight quotes in JSON; verify UTF-8 encoding without BOM

Failure Points in Production

Anti-Patterns

Wrong: Using OData $filter on large datasets instead of BAQ parameters

# 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

Correct: Use BAQ parameters for database-level filtering

# 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

Wrong: Calling BAQ API without pagination for unknown result sizes

# 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!

Correct: Always implement pagination loop

# 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

Wrong: Modifying a shared BAQ that has external API consumers

# 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.

Correct: Version BAQs and maintain backward compatibility

# 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.

Common Pitfalls

Diagnostic Commands

# 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"]'

Version History & Compatibility

API VersionRelease DateStatusBreaking ChangesMigration Notes
REST v2 (Kinetic 2022.2+)2022-09CurrentOData query parameter support added; new URL patternUse /api/v2/odata/ for new integrations
REST v2 (Kinetic 2024.1)2024-03CurrentToken endpoint standardizedTokenResource.svc endpoint for bearer tokens
REST v12016Supported (legacy)Limited OData supportUse /api/v1/BaqSvc/ format; migration to v2 recommended

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Extracting data for dashboards, reports, or BI toolsBuilding complex business logic that modifies multiple related tablesEpicor Business Objects (BO) REST API or EFx Functions
Providing read-only data feeds to external applicationsOrchestrating multi-step transactionsEpicor BPM / EFx Functions with proper transaction handling
Connecting Excel or Power BI to live Epicor dataNeed real-time push notifications when data changesEpicor Service Connect / BPM Event-Driven patterns
Simple CRUD on single-table updatable BAQsBulk data migration (>100K records)Epicor DMT (Data Migration Tool) or direct database ETL

Important Caveats

Related Units