---
# === IDENTITY ===
id: business/erp-integration/epicor-baq-api-endpoints/2026
canonical_question: "How do you expose Epicor Business Activity Queries (BAQ) as API endpoints?"
aliases:
  - "How to call Epicor BAQ from REST API"
  - "Epicor BAQ OData endpoint URL format and authentication"
  - "Epicor Kinetic BAQ REST v2 API configuration and access scope setup"
  - "How to expose Epicor BAQ as external REST API with API key"
entity_type: erp_integration
domain: business > erp-integration > epicor-baq-api-endpoints
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Epicor Kinetic (ERP)"
    vendor: "Epicor"
    version: "REST API v2 (Kinetic 2022.2+)"
    edition: "All editions (Standard, Enterprise, Cloud, On-Premise)"
    deployment: "hybrid"
    api_surface: "REST/OData v4"

# === VERIFICATION ===
last_verified: 2026-03-02
confidence: 0.88
version: 1.0
first_published: 2026-03-02

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "2022.2 — REST Services v2 introduced, v1 still supported"
  next_review: 2026-08-29
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "Default max row count is 100 per request; configurable via DefaultMaxRowCount in web.config (on-premise only)"
  - "Cloud/SaaS customers cannot modify web.config — must use pagination ($top/$skip) for large result sets"
  - "ServerFileDownload endpoint has a 2 GB file size limit (System.IO limitation)"
  - "MaxAllowedContentLength is approximately 4 GB — practical limit for very large BAQ result sets"
  - "API keys are shown only once at creation time — cannot be retrieved afterward"
  - "BAQ parameters execute at the database level; OData $filter is post-processing — use parameters for performance-critical queries"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs standard Epicor Business Object (BO) REST API, not BAQ-specific endpoints"
    use_instead: "business/erp-integration/epicor-rest-api/2026"
  - condition: "User needs Epicor BPM/Function (EFx) REST endpoints, not BAQ queries"
    use_instead: "business/erp-integration/epicor-efx-functions/2026"
  - condition: "User needs to build a BAQ in the Epicor UI, not expose it as an API"
    use_instead: "N/A — see Epicor BAQ Designer documentation"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "read-only data extraction (GET BAQ results)"
      - "updatable BAQ (CRUD via REST)"
      - "parameterized query (dynamic filtering)"
      - "OData feed for BI tools (Excel, Power BI, Grafana)"
  - key: deployment
    question: "What is the Epicor deployment model?"
    type: choice
    options:
      - "cloud/SaaS (Epicor-hosted)"
      - "on-premise (self-hosted)"
  - key: auth_method
    question: "Which authentication method do you prefer?"
    type: choice
    options:
      - "API key (simplest, recommended for service integrations)"
      - "Basic Authentication (username/password)"
      - "Bearer token (session-based, 8-hour lifetime)"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/epicor-baq-api-endpoints/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-02)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/epicor-rest-api/2026"
      label: "Epicor Kinetic REST API overview and rate limits"
  solves:
    - id: "business/erp-integration/epicor-data-extraction/2026"
      label: "Data extraction playbooks using Epicor BAQ endpoints"
  alternative_to:
    - id: "business/erp-integration/epicor-efx-functions/2026"
      label: "Epicor EFx Functions as API endpoints (custom logic, not queries)"
  often_confused_with:
    - id: "business/erp-integration/epicor-bo-rest-api/2026"
      label: "Standard Epicor Business Object REST API (not BAQ-specific)"

# === SOURCES ===
sources:
  - id: src1
    title: "How to Use BAQs in Epicor: Part 5 — Integrating BAQs with REST"
    author: Automatid
    url: https://automatid.io/2024/08/16/how-to-use-baqs-in-epicor-part-5-integrating-baqs-with-rest/
    type: technical_blog
    published: 2024-08-16
    reliability: high
  - id: src2
    title: "Pulling BAQ into Excel — Web API, OData, JSON? (Epicor User Help Forum)"
    author: Epicor Community
    url: https://www.epiusers.help/t/pulling-baq-into-excel-web-api-odata-json/102015
    type: community_resource
    published: 2024-01-15
    reliability: moderate_high
  - id: src3
    title: "BAQ UI & BAQSvc REST API Limitations (Epicor User Help Forum)"
    author: Epicor Community
    url: https://www.epiusers.help/t/baq-ui-baqsvc-rest-api-limitations/107024
    type: community_resource
    published: 2024-06-10
    reliability: moderate_high
  - id: src4
    title: "Importing Epicor Software Data into Excel Using REST Services"
    author: Arris Consulting
    url: https://arrisconsulting.com/blog/importing-epicor-software-data-into-excel-using-rest-services/
    type: technical_blog
    published: 2024-03-20
    reliability: moderate_high
  - id: src5
    title: "REST/EFx/BAQ Examples from a Python Script (Epicor User Help Forum)"
    author: Epicor Community
    url: https://www.epiusers.help/t/rest-efx-baq-examples-from-a-python-script/115273
    type: community_resource
    published: 2024-09-15
    reliability: moderate_high
  - id: src6
    title: "REST API Bearer Token Authentication (Epicor User Help Forum)"
    author: Epicor Community
    url: https://www.epiusers.help/t/rest-with-bearer-tokens-clientid-and-client-secret-s/126627
    type: community_resource
    published: 2025-03-10
    reliability: moderate
  - id: src7
    title: "Filter Syntax for BAQ Using REST API (Epicor User Help Forum)"
    author: Epicor Community
    url: https://www.epiusers.help/t/filter-syntax-for-baq-using-rest-api/113116
    type: community_resource
    published: 2024-08-01
    reliability: moderate_high
  - id: src8
    title: "11.2 REST API Pagination (Epicor User Help Forum)"
    author: Epicor Community
    url: https://www.epiusers.help/t/11-2-rest-api-pagination/101272
    type: community_resource
    published: 2024-01-10
    reliability: moderate_high
---

# Epicor BAQ API Endpoints: Exposing Business Activity Queries via REST/OData

## TL;DR

- **Bottom line**: Epicor BAQs are exposed as REST/OData endpoints at `/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]
- **Key limit**: Default max row count is 100 per request; use `$top` and `$skip` for pagination. Cloud/SaaS users cannot override this via web.config. [src3, src8]
- **Watch out for**: BAQ parameters filter at the database level while OData `$filter` applies post-processing — use BAQ parameters for large datasets to avoid memory issues. [src7]
- **Best for**: Read-only data extraction, BI tool integration (Excel, Power BI, Grafana), and custom application data access from Epicor ERP.
- **Authentication**: API key (header or query parameter), Basic Auth (username/password), or Bearer token via `TokenResource.svc` (8-hour lifetime). [src1, src6]

## 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. REST v1 endpoints are also functional but v2 is the current recommended version. [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 URL** | [Swagger UI](https://{server}/{instance}/api/help/v2/) (per-instance) |
| **Status** | GA |

## 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 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 |

## Rate Limits & Quotas

### Per-Request Limits

| 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 | 1 row per PATCH | Updatable BAQ updates | Pass one changed row at a time to the Data method [src5] |

[src3, src8]

### Rolling / Daily Limits

| 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] |

[src3, src5]

## 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]

| 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 System Setup > Security Maintenance > API Key Maintenance. [src1, src2] |
| Basic Authentication | Quick testing, internal tools | Session-based | N/A | `Authorization: Basic {base64(user:pass)}`. Each request creates a new session. [src4, src5] |
| 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. [src6] |

[src1, src6]

### Authentication Gotchas

- **API keys must be paired with Access Scopes**: Creating an API key alone is insufficient — you must create an Access Scope that includes `Erp.BO.BAQSvc` in the services list AND add the specific BAQ name to the BAQs list. Without this, the API returns 403. [src1]
- **API key is shown only once at creation**: After saving the API key in Epicor, the key value is never shown again. Copy it immediately and store securely. [src1]
- **Bearer tokens still require API key headers**: Even when using Bearer token authentication, you may still need to send the `X-API-Key` header on subsequent requests depending on your configuration. [src6]
- **Sessions consume licenses**: Each authenticated API session (especially Basic Auth) consumes a Client Access License (CAL). Close sessions when done, or use API key auth which shares a session. [src5]

## Constraints

- **Cloud/SaaS customers cannot modify `DefaultMaxRowCount`** — the 100-row default per request is effectively fixed; pagination via `$top`/`$skip` is mandatory for larger result sets. [src3]
- **OData `$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 of `$filter` for performance. [src7]
- **Updatable BAQs accept one row per PATCH** — you cannot batch multiple row updates in a single request. Loop through rows individually. [src5]
- **Excel strips query parameters from OData URLs** — when connecting Excel to OData v2 endpoints, Excel's native connector removes query strings including `?api-key=`, forcing fallback to Basic Auth for Excel integrations. [src2]
- **BAQ security inherits from Epicor table/field security** — the API cannot bypass security configured in the BAQ Designer or at the Epicor security level. [src4]
- **REST v1 and v2 coexist** — v1 uses `/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]

## 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

| 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 [src2] |
| Execute BAQ (REST) | GET | `/api/v2/baq/{BaqName}` | `X-API-Key: {key}` | Returns standard JSON [src1] |
| Execute BAQ with params | GET | `/api/v2/odata/{Co}/BaqSvc/{BAQ}/Data?Param1=Value1` | `X-API-Key: {key}` | BAQ parameters as query params [src1] |
| Filter results (OData) | GET | `.../Data?$filter=Field eq 'Value'` | `X-API-Key: {key}` | Post-processing filter [src7] |
| Paginate results | GET | `.../Data?$top=100&$skip=200` | `X-API-Key: {key}` | Third page of 100 rows [src8] |
| 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 [src5] |
| Update row (updatable) | PATCH | `/api/v2/baq/{BaqName}/Data` | `X-API-Key: {key}` | Pass single row as {"ds": {...}} [src5] |
| Get Bearer token | POST | `/{instance}/TokenResource.svc` | `username` + `password` headers | Returns JWT valid for 8 hours [src6] |
| Access Swagger help | GET | `/api/help/v2/` | Browser session | Interactive 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. Test the query in the BAQ Designer to confirm correct results. [src1, src4]

```plaintext
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]

```plaintext
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. This key will authenticate all external API requests. [src1]

```plaintext
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 (password manager, secrets vault)

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]

```bash
# Input:  Epicor server URL, BAQ name, API key
# Output: BAQ results as JSON

# OData endpoint (recommended for data feeds)
curl -s "https://your-epicor-server/YourInstance/api/v2/odata/YourCompany/BaqSvc/MyCustomBAQ/Data" \
  -H "X-API-Key: your-api-key-here" \
  -H "Accept: application/json"

# REST endpoint (alternative)
curl -s "https://your-epicor-server/YourInstance/api/v2/baq/MyCustomBAQ" \
  -H "X-API-Key: your-api-key-here" \
  -H "Accept: application/json"

# With BAQ parameter
curl -s "https://your-epicor-server/YourInstance/api/v2/odata/YourCompany/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]

```bash
# Input:  OData BAQ endpoint, desired page size
# Output: Paginated results

# 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"

# Page 3 (rows 200-299)
curl -s ".../BaqSvc/MyCustomBAQ/Data?$top=100&$skip=200" \
  -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 on large datasets, use BAQ parameters instead. [src7]

```bash
# Input:  OData BAQ endpoint, filter criteria
# Output: Filtered and shaped results

# Filter with equality
curl -s ".../BaqSvc/MyCustomBAQ/Data?\$filter=PartPlant_PersonID eq 'JSMITH'" \
  -H "X-API-Key: your-api-key-here"

# Filter with comparison
curl -s ".../BaqSvc/MyCustomBAQ/Data?\$filter=SellingQuantity gt 0" \
  -H "X-API-Key: your-api-key-here"

# Select specific fields only
curl -s ".../BaqSvc/MyCustomBAQ/Data?\$select=OrderNum,CustomerName,OrderTotal" \
  -H "X-API-Key: your-api-key-here"

# Combine filter, select, and pagination
curl -s ".../BaqSvc/MyCustomBAQ/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

```python
# 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  # Last page reached
        skip += page_size

    return all_rows

# Usage: fetch all rows
results = get_baq_data(BAQ_NAME)
print(f"Total rows: {len(results)}")

# Usage: with BAQ parameter
results = get_baq_data(BAQ_NAME, params={"CustomerID": "ACME001"})
```

### JavaScript/Node.js: Execute BAQ with API key

```javascript
// Input:  Epicor server URL, company, BAQ name, API key
// Output: BAQ results as JSON

// npm install node-fetch@2
const fetch = require("node-fetch");

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`
  );

  // Add OData params
  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 || [];
}

// Usage
(async () => {
  const rows = await getBAQData("MyCustomBAQ", {
    "$top": "100",
    "$filter": "OrderTotal gt 1000",
    "$select": "OrderNum,CustomerName,OrderTotal",
  });
  console.log(`Rows returned: ${rows.length}`);
  console.log(rows);
})();
```

### cURL: Quick BAQ API test with all auth methods

```bash
# Input:  Epicor server URL, credentials or API key
# Output: BAQ results as JSON

# === 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: API Key as query parameter ===
curl -s "https://server/instance/api/v2/odata/Company/BaqSvc/MyBAQ/Data?api-key=your-api-key" \
  -H "Accept: application/json" | jq .

# === Method 3: Basic Authentication ===
curl -s "https://server/instance/api/v2/odata/Company/BaqSvc/MyBAQ/Data" \
  -u "username:password" \
  -H "Accept: application/json" | jq .

# === Method 4: Bearer Token ===
# Step 1: Get 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')

# Step 2: Use token
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

| Component | Value | Example | Notes |
|---|---|---|---|
| Base URL | `https://{server}/{instance}` | `https://centralusdtapp01.epicorsaas.com/saas512` | Varies by deployment [src2] |
| OData BAQ path | `/api/v2/odata/{Company}/BaqSvc/{BaqName}/Data` | `/api/v2/odata/MYCO/BaqSvc/SalesOrders/Data` | Recommended for data feeds [src2] |
| REST BAQ path | `/api/v2/baq/{BaqName}` | `/api/v2/baq/SalesOrders` | Alternative format [src1] |
| REST v1 path | `/api/v1/BaqSvc/{BaqName}` | `/api/v1/BaqSvc/SalesOrders` | Legacy, still functional [src4] |
| Swagger help | `/api/help/v2/` | `/api/help/v2/` | Interactive docs [src1] |
| Token endpoint | `/{instance}/TokenResource.svc` | `/saas512/TokenResource.svc` | Bearer token acquisition [src6] |

### Data Type Gotchas

- **OData field names use Table_Field format**: BAQ OData results prefix field names with the table name and underscore (e.g., `OrderHed_OrderNum`, `Customer_Name`), which differs from the display names in the BAQ Designer. [src2]
- **Date fields return ISO 8601 format**: All date/datetime fields in BAQ results are returned in ISO 8601 format (e.g., `2026-03-02T00:00:00Z`), regardless of the user's Epicor date format preference. [src5]
- **Calculated fields use `Calculated_` prefix**: Any calculated columns in the BAQ appear with a `Calculated_` prefix in API results (e.g., `Calculated_TotalWithTax`). [src2]
- **Boolean fields return true/false as strings in some versions**: Older REST v1 endpoints may return boolean values as strings (`"true"`) rather than JSON booleans (`true`). REST v2 uses proper JSON types. [src5]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 Unauthorized | Invalid or missing authentication | API key not provided, expired, or invalid; wrong username/password | Verify API key exists and is correctly copied; check Access Scope includes the BAQ [src1] |
| 403 Forbidden | Access denied | API key exists but Access Scope does not include `Erp.BO.BAQSvc` or the specific BAQ | Add `Erp.BO.BAQSvc` to Access Scope services AND add the BAQ name to the BAQs list [src1] |
| 404 Not Found | BAQ or endpoint not found | BAQ name misspelled, not shared, or wrong company ID in URL | Verify BAQ name in BAQ Designer; check company ID matches; use Swagger help to confirm endpoint [src1] |
| 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 [src3] |
| 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 [src5] |

[src1, src3, src5]

### Failure Points in Production

- **API key rotation with no notification**: Epicor does not send expiration warnings for API keys. If a key is rotated or deleted by an admin, integrations break silently. Fix: `Document all API keys in a secrets manager with ownership and rotation schedule. Monitor for 401 errors.` [src1]
- **BAQ modification breaks API consumers**: When someone edits a shared BAQ in the designer (adding/removing/renaming columns), all API consumers using that BAQ break with changed field names or missing data. Fix: `Version your BAQs (e.g., SalesOrders_v1, SalesOrders_v2). Never modify a BAQ that has external consumers — create a new version instead.` [src3]
- **Memory exhaustion on unfiltered large BAQs**: Calling a BAQ without parameters or filters on a large table (e.g., OrderDtl with millions of rows) loads the entire result into server memory before pagination. Fix: `Always add BAQ parameters for large tables. Use $top to limit initial requests. Never deploy a BAQ without WHERE criteria on high-volume tables.` [src3, src7]
- **Session license exhaustion**: Each Basic Auth request creates a new Epicor session consuming a CAL. High-frequency API calls can exhaust all available licenses. Fix: `Use API key authentication (shares sessions). If using Basic Auth, use the requests.Session() pattern in Python to reuse sessions. Close sessions explicitly.` [src5]
- **NGINX proxy update breaking BAQ APIs**: Epicor SaaS infrastructure updates (particularly NGINX reverse proxy changes) can alter URL handling and break existing BAQ API integrations. Fix: `Monitor Epicor release notes and SaaS infrastructure announcements. Test API endpoints after each Epicor update window.` [src3]

## Anti-Patterns

### Wrong: Using OData $filter on large datasets instead of BAQ parameters

```python
# 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

```python
# GOOD — BAQ parameters filter at the DB level before results are loaded
# The BAQ is designed with a parameter "CustomerID" in BAQ Designer
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

```python
# 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

```python
# 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

```plaintext
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

```plaintext
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

- **Forgetting to add BAQ to Access Scope BAQs list**: Creating an Access Scope with `Erp.BO.BAQSvc` is necessary but not sufficient — you must also explicitly add each BAQ name to the scope's BAQs list. Without this, the API returns 403 even with a valid API key. Fix: `Open Access Scope Maintenance, navigate to the BAQs tab, and add each BAQ that should be externally accessible.` [src1]
- **Using REST v1 URL format with v2 features**: OData query parameters (`$filter`, `$select`, `$top`, `$skip`) may not work correctly on REST v1 endpoints. Fix: `Always use v2 endpoints (/api/v2/odata/...) for OData query parameter support.` [src4]
- **Excel stripping API key from OData URLs**: Excel's OData data source connector strips query strings from URLs, removing the `?api-key=` parameter. Fix: `Use Basic Authentication when connecting Excel to Epicor OData endpoints, or use Power Query with a custom connector that sets the X-API-Key header.` [src2]
- **Not URL-encoding filter values**: OData `$filter` values with spaces or special characters must be URL-encoded. The Swagger UI does this automatically, but external code does not. Fix: `Use URL encoding libraries (urllib.parse.quote in Python, encodeURIComponent in JavaScript) for all filter values.` [src7]
- **Assuming BAQ field names match UI labels**: API response field names use `Table_Column` format (e.g., `OrderHed_OrderNum`), not the display labels shown in the BAQ Designer or Epicor forms. Fix: `Use the Swagger help page or a test API call to identify exact field names before building integrations.` [src2]
- **Not handling Epicor session/license consumption**: Each Basic Auth API request may create a new session, consuming a CAL license. Heavy API usage can lock out interactive users. Fix: `Use API key authentication for automated integrations. If using session-based auth, implement session pooling and explicit cleanup.` [src5]

## Diagnostic Commands

```bash
# 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)

# Verify BAQ exists and is accessible via Swagger
curl -s "https://server/instance/api/help/v2/" \
  -u "username:password" | grep -i "BaqSvc"

# 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 (paginate to count)
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 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 [src1] |
| REST v2 (Kinetic 2024.1) | 2024-03 | Current | Token endpoint standardized | `TokenResource.svc` endpoint for bearer tokens [src6] |
| REST v1 | 2016 | Supported (legacy) | Limited OData support | Use `/api/v1/BaqSvc/` format; migrating to v2 recommended [src4] |

[src1, src4, src6]

### Deprecation Policy

Epicor maintains backward compatibility for REST v1 endpoints alongside REST v2. There is no announced deprecation date for v1, but all new features and documentation target v2. Epicor SaaS infrastructure updates (NGINX, IIS) can introduce breaking changes with limited advance notice — monitor Epicor release notes and EpicCare advisories. [src1, src3]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Extracting data from Epicor 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 (e.g., create SO + allocate inventory + ship) | 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 |
| External API consumers need curated, secure data views | Full database access bypassing Epicor security model | Not recommended — use BAQs to enforce security |

## Important Caveats

- BAQ REST endpoints inherit all Epicor table-level and field-level security — the API cannot return data the authenticated user/key does not have access to. Ensure the API key's associated user has appropriate security group assignments. [src1, src4]
- On-premise and cloud/SaaS deployments differ significantly in configuration options: web.config tuning (DefaultMaxRowCount, MaxAllowedContentLength) is only available on-premise. Cloud customers must work within default limits using pagination. [src3]
- Epicor does not publish formal API rate limits — practical throughput depends on server hardware, concurrent users, and query complexity. Heavy BAQ API usage on shared SaaS infrastructure can trigger throttling with no prior warning. [src3]
- BAQ performance via API is directly tied to the BAQ's SQL efficiency. A poorly designed BAQ (missing indexes, full table scans, excessive joins) will be slow via API regardless of network or client optimization. Always optimize the BAQ in the designer first. [src7]
- Epicor SaaS infrastructure updates (NGINX, IIS, load balancer changes) can break BAQ API integrations without direct notification. Subscribe to Epicor EpicCare and release notes for advance warning. [src3]

## Related Units

- [Epicor Kinetic REST API overview](/business/erp-integration/epicor-rest-api/2026)
- [Epicor EFx Functions as API endpoints](/business/erp-integration/epicor-efx-functions/2026)
- [Epicor standard Business Object REST API](/business/erp-integration/epicor-bo-rest-api/2026)
