---
# === IDENTITY ===
id: business/erp-integration/epicor-rest-api-v2/2026
canonical_question: "What are the Epicor Kinetic REST API v2 capabilities, OData patterns, and BAQ exposures?"
aliases:
  - "How to use Epicor Kinetic REST API v2 with OData and Business Activity Queries"
  - "Epicor Kinetic ERP API v2 endpoint patterns, BAQ access, and EFx functions"
  - "Epicor REST v2 OData query options, authentication, and Business Object services"
  - "Epicor Kinetic API v2 vs v1 differences, BAQ REST endpoints, and rate limits"
entity_type: erp_integration
domain: business > erp-integration > epicor-rest-api-v2
region: global
jurisdiction: global
temporal_scope: 2022-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Epicor Kinetic"
    vendor: "Epicor"
    version: "REST API v2 (Kinetic 2022.2+)"
    edition: "All editions (Cloud, On-Premise, Hybrid)"
    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 v2 introduced with mandatory Company in URL path"
  next_review: 2026-08-29
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "API Key is required for all REST v2 requests by default (EnforceApiKeyForRestApiV2 setting)"
  - "OData protocol is only supported for Business Object (BO) service types -- Lib, Proc, and Rpt services support custom methods only"
  - "Default maximum of 100 rows returned per OData query -- configurable via DefaultMaxRowCount in web.config (on-premise only)"
  - "HTTPS/SSL is mandatory for all REST API requests"
  - "Cloud/SaaS environments cannot modify web.config settings like DefaultMaxRowCount"
  - "No built-in rate limiting in Epicor -- must be configured at IIS or reverse proxy level"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs Epicor REST API v1 (legacy, no OData, no Company in URL)"
    use_instead: "N/A - v1 is deprecated; migrate to v2"
  - condition: "User needs Epicor Prophet 21 API"
    use_instead: "business/erp-integration/epicor-prophet-21-api/2026"
  - condition: "User needs SOAP/WCF services for Epicor"
    use_instead: "N/A - REST v2 replaces WCF; use REST v2 for all new development"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (BPM directives, EFx triggers)"
      - "reporting/analytics (BAQ queries, read-only)"
  - key: data_volume
    question: "What's your daily data volume?"
    type: choice
    options:
      - "< 1,000 records/day"
      - "1,000-100,000 records/day"
      - "> 100,000 records/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to Epicor)"
      - "outbound (reading from Epicor)"
      - "bidirectional sync"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/epicor-rest-api-v2/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-kinetic-authentication/2026"
      label: "Epicor Kinetic authentication methods (API key, bearer token, Azure AD)"
  solves:
    - id: "business/erp-integration/epicor-netsuite-integration/2026"
      label: "Epicor to NetSuite integration playbook"
  alternative_to:
    - id: "business/erp-integration/epicor-prophet-21-api/2026"
      label: "Epicor Prophet 21 API (different product line)"
  often_confused_with:
    - id: "business/erp-integration/epicor-rest-api-v1/2026"
      label: "Epicor REST API v1 (legacy, no OData, different URL pattern)"

# === SOURCES ===
sources:
  - id: src1
    title: "Epicor REST Services v2 Technical Documentation (10.2.700+)"
    author: Epicor
    url: https://pdfcoffee.com/epicor10-restservices-v2-102700-pdf-free.html
    type: official_docs
    published: 2022-06-15
    reliability: authoritative
  - id: src2
    title: "REST Services V2 - Epicor User Help Forum"
    author: Epicor Community
    url: https://www.epiusers.help/t/rest-services-v2/57725
    type: community_resource
    published: 2020-03-15
    reliability: moderate_high
  - id: src3
    title: "REST Authentication with SaaS - Epicor User Help Forum"
    author: Epicor Community
    url: https://www.epiusers.help/t/rest-authentication-with-saas/106150
    type: community_resource
    published: 2023-08-10
    reliability: moderate_high
  - id: src4
    title: "REST API Bearer Token - Epicor User Help Forum"
    author: Epicor Community
    url: https://www.epiusers.help/t/rest-api-bearer-token/122686
    type: community_resource
    published: 2024-06-15
    reliability: moderate_high
  - id: src5
    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: 2023-09-20
    reliability: moderate_high
  - id: src6
    title: "Epicor REST API Rate Limiting - Epicor User Help Forum"
    author: Epicor Community
    url: https://www.epiusers.help/t/epicor-rest-api-rate-limiting/51801
    type: community_resource
    published: 2019-10-15
    reliability: moderate
  - id: src7
    title: "REST/EFx/BAQ Examples from 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-02-10
    reliability: moderate_high
  - id: src8
    title: "Epycor - Python package for Epicor/Kinetic ERP REST API"
    author: Kopachris
    url: https://github.com/Kopachris/Epycor
    type: community_resource
    published: 2024-01-20
    reliability: moderate
---

# Epicor Kinetic REST API v2: Capabilities, OData Patterns, and BAQ Exposures

## TL;DR

- **Bottom line**: Epicor Kinetic REST API v2 is the primary integration surface for Epicor ERP, providing OData v4 compliant endpoints for Business Objects (BO), Business Activity Queries (BAQ), and Epicor Functions (EFx). It replaced v1 starting in Epicor 10.2.700 / Kinetic 2022.2. [src1]
- **Key limit**: Default 100-row maximum per OData query response (configurable via `DefaultMaxRowCount` in web.config for on-premise; cloud environments cannot change this). No built-in rate limiting -- throttling must be configured at IIS or reverse proxy level. [src1, src6]
- **Watch out for**: REST v2 requires both authentication (Basic/Bearer) AND an API Key on every request. Missing either one produces different error messages that can be confusing to debug. [src1, src2]
- **Best for**: Real-time CRUD operations on Business Objects, exposing BAQ data to external systems, and invoking custom Epicor Functions (EFx) for serverless logic.
- **Authentication**: API Key required on all requests (header or query parameter) + Basic Auth, Bearer Token (via TokenResource.svc), Azure AD, or Epicor IdP for user identity. [src1, src3, src4]

## System Profile

Epicor Kinetic (formerly Epicor ERP 10) is a mid-market manufacturing ERP system. REST API v2 was introduced in Epicor 10.2.700 (2020) and became the standard API surface in Kinetic 2022.2. It replaces the legacy v1 REST API and WCF SOAP services. The v2 API adds OData v4 compliance, mandatory Company-scoped URLs, and API Key enforcement. This card covers all deployment models: cloud (Epicor SaaS), on-premise, and hybrid. Cloud/SaaS deployments have restrictions on server-side configuration (e.g., cannot modify web.config). [src1]

| Property | Value |
|---|---|
| **Vendor** | Epicor |
| **System** | Epicor Kinetic (ERP 10.2.700+) |
| **API Surface** | REST/OData v4 |
| **Current API Version** | v2 (Kinetic 2022.2 through 2025.x) |
| **Editions Covered** | All -- Cloud (SaaS), On-Premise, Hybrid |
| **Deployment** | Cloud / On-Premise / Hybrid |
| **API Docs URL** | [Epicor REST Help](https://{server}/{instance}/api/help/v2/) (per-instance; interactive Swagger) |
| **Status** | GA |

## API Surfaces & Capabilities

Epicor Kinetic exposes four distinct service types through the REST API v2, but only Business Objects (BO) support full OData query capabilities. Library, Process, and Report services are limited to custom method invocation via RPC-style calls. [src1]

| API Surface | Protocol | Best For | OData Support | Real-time? | Endpoint Pattern |
|---|---|---|---|---|---|
| Business Objects (BO) | HTTPS/JSON (OData v4) | CRUD on standard ERP entities (Customers, Parts, Sales Orders) | Full ($select, $filter, $expand, $orderby, $top, $skip) | Yes | `/api/v2/odata/{Company}/Erp.BO.{Service}/{EntitySet}` |
| BAQ Services (BaqSvc) | HTTPS/JSON (OData v4) | Read-only and updatable custom queries | Full ($select, $filter, $orderby, $top, $skip) | Yes | `/api/v2/odata/{Company}/BaqSvc/{BaqName}/Data` |
| Epicor Functions (EFx) | HTTPS/JSON (RPC) | Custom serverless logic, webhooks, automation | No (POST only with JSON body) | Yes | `/api/v2/efx/{Company}/{Library}/{Function}/` |
| Library/Process/Report | HTTPS/JSON (RPC) | Custom methods, batch processes, report generation | No (custom methods only via POST) | Varies | `/api/v2/odata/{Company}/Ice.{Type}.{Service}/{Method}` |

[src1]

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Default max rows per query | 100 | OData queries (BO and BAQ) | Configurable via `DefaultMaxRowCount` in web.config; set to 0 for unlimited (on-premise only) [src1] |
| Max content length | ~4 GB (4,294,967,295 bytes) | HTTP response body | Practical limit; large responses may timeout before reaching this [src5] |
| ServerFileDownload limit | 2 GB | File download endpoint | Use for large BAQ exports in cloud environments [src5] |
| Max batch file size | No documented limit | OData $batch | Each subrequest within $batch is processed independently |

[src1, src5]

### Rolling / Daily Limits

| Limit Type | Value | Window | Notes |
|---|---|---|---|
| API rate limiting | None built-in | N/A | No native rate limiting in Epicor; must configure at IIS (Dynamic IP Security) or reverse proxy [src6] |
| Concurrent connections | Server-dependent | Per-IIS instance | Controlled by IIS application pool settings and available server resources [src6] |
| Web Services licensing | License-based throttling | Per-session | When WS licenses exhausted, Epicor progressively doubles response delay for additional requests [src6] |
| Session timeout | Configurable | Per-session | Sessions must be properly closed via Logout; orphaned sessions consume server resources |

[src6]

## Authentication

Epicor Kinetic REST API v2 uses a dual-layer authentication model: every request requires an API Key for access scope control, plus a user identity mechanism (Basic Auth, Bearer Token, Azure AD, or Epicor IdP). [src1, src2]

| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Basic Auth + API Key | Simple integrations, testing | Per-request (stateless) | N/A | Username:password in Authorization header + API Key. Credentials sent on every request. [src1, src2] |
| Bearer Token + API Key | Production integrations, reduced credential exposure | Configurable (default ~1h, up to 100h+) | No refresh token; request new before expiry | POST to TokenResource.svc with Basic Auth; use returned JWT for subsequent calls. [src3, src4] |
| Azure AD + API Key | Cloud/SaaS with enterprise SSO | Azure AD token lifetime | Yes (via Azure AD) | Epicor cloud only. OAuth 2.0 flow through Azure AD/Entra ID. [src3] |
| Epicor IdP + API Key | Cloud/SaaS with Epicor identity | IdP token lifetime | Yes (via IdP) | Epicor cloud only. Provides MFA support. [src3] |
| Windows SSO + API Key | On-premise with Active Directory | Windows session | N/A | On-premise only. Uses Windows integrated authentication. [src1] |

[src1, src3, src4]

### Authentication Gotchas

- **Dual authentication is mandatory**: Both API Key AND user identity (Basic/Bearer/Azure AD) are required on every request. Missing API Key returns "Access denied: Rest calls must pass a valid API Key"; missing user credentials returns "401 - Unauthorized: Access is denied due to invalid credentials". These are different errors that require different fixes. [src1, src2]
- **API Key does not map to a user account**: The API Key controls access scope (which services/methods are accessible), but the user identity (Basic Auth or Bearer Token) determines the security context (which data is visible). A valid API Key alone does not authenticate a user. [src2]
- **Bearer tokens change daily**: Do not cache or hardcode bearer tokens. They expire based on the admin-configured timeout (default 1 hour). Implement token renewal logic that checks expiry before each batch of requests. [src4]
- **TokenResource.svc requires POST, not GET**: A common mistake is trying to GET a token from TokenResource.svc, which returns 405 Method Not Allowed. The endpoint only accepts POST with username/password in the request headers. [src4]
- **Cloud SaaS environments expose TokenResource.svc publicly**: Unlike on-premise where you can restrict access, SaaS token endpoints are internet-accessible. Use Azure AD or Epicor IdP for stronger security in cloud deployments. [src3]

## Constraints

- **OData is only for Business Objects (BO)** -- Library (Lib), Process (Proc), and Report (Rpt) service types do not support OData query options ($select, $filter, etc.). They only support custom method invocation via POST. [src1]
- **Cloud environments cannot modify DefaultMaxRowCount** -- SaaS customers are stuck with the 100-row default unless they use pagination ($top + $skip) or the ServerFileDownload endpoint. [src1, src5]
- **API Key enforcement can be disabled but shouldn't be** -- The `EnforceApiKeyForRestApiV2` setting can be set to false (sometimes needed for Excel OData connectivity), but this removes access scope control entirely. [src1]
- **v1 and v2 APIs coexist** -- Both are available simultaneously, which creates confusion. v1 URLs (`/api/v1/`) do not require Company in the path or API Key. New development should always use v2. [src2]
- **Session management is critical** -- Each authenticated REST call creates a server session. Failing to call Logout consumes server resources. For high-volume integrations, reuse sessions or use Bearer tokens to minimize session overhead. [src1]
- **JSON only in v2** -- REST API v2 returns JSON exclusively (OData v4 standard). XML/Atom responses from v1 are not available in v2. [src1]

## Integration Pattern Decision Tree

```
START -- Integrate with Epicor Kinetic REST API v2
|-- What service type do you need?
|   |-- Standard ERP entity CRUD (Customers, Parts, Orders, etc.)
|   |   --> Business Object (BO) endpoint with OData
|   |   |-- Read single record?
|   |   |   --> GET /api/v2/odata/{Co}/Erp.BO.{Svc}/{Entity}('{Key}')
|   |   |-- Read with filters?
|   |   |   --> GET with $filter, $select, $top, $skip
|   |   |-- Create/Update/Delete?
|   |   |   --> POST (create), PATCH (update), DELETE (delete)
|   |   |-- Custom method (GetByID, Update, etc.)?
|   |       --> POST /api/v2/odata/{Co}/Erp.BO.{Svc}/{Method}
|   |-- Custom query / reporting?
|   |   --> BAQ Service (BaqSvc) endpoint
|   |   |-- Simple read?
|   |   |   --> GET /api/v2/odata/{Co}/BaqSvc/{BAQ}/Data
|   |   |-- Parameterized?
|   |   |   --> GET .../{BAQ}/Data?param1='value1'
|   |   |-- Need to write back?
|   |       --> Updatable BAQ with PATCH
|   |-- Custom logic / serverless function?
|   |   --> Epicor Function (EFx) endpoint
|   |   --> POST /api/v2/efx/{Co}/{Library}/{Function}/
|   |-- Batch process or report?
|       --> Library/Process/Report service (RPC only)
|-- What volume?
|   |-- < 100 records --> Single OData query (within default row limit)
|   |-- 100-10,000 records --> Paginate with $top + $skip
|   |-- > 10,000 records --> ServerFileDownload or chunked pagination
|-- Authentication?
|   |-- On-premise, simple --> Basic Auth + API Key
|   |-- On-premise, production --> Bearer Token + API Key
|   |-- Cloud/SaaS --> Azure AD or Epicor IdP + API Key [src3]
|   |-- High-volume batch --> Bearer Token (avoid per-request credential exchange)
```

## Quick Reference

| Operation | Method | Endpoint Pattern | Notes |
|---|---|---|---|
| List BO entity records | GET | `/api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers` | Returns OData collection; default 100 rows [src1] |
| Get single record by key | GET | `/api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers('{CustID}',{CustNum})` | Composite key in parentheses [src1] |
| Create record | POST | `/api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers` | JSON body with required fields |
| Update record | PATCH | `/api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers('{CustID}',{CustNum})` | Only changed fields in body |
| Delete record | DELETE | `/api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers('{CustID}',{CustNum})` | Returns 204 on success |
| Invoke BO method | POST | `/api/v2/odata/{Co}/Erp.BO.CustomerSvc/GetByID` | JSON body: `{"custNum": 123}` |
| Query BAQ | GET | `/api/v2/odata/{Co}/BaqSvc/{BaqName}/Data` | OData query options apply [src1] |
| BAQ with parameters | GET | `/api/v2/odata/{Co}/BaqSvc/{BaqName}/Data?param1='val'` | Parameters as query strings |
| List all BAQs | GET | `/api/v2/odata/{Co}/BaqSvc` | Returns service document listing BAQs |
| BAQ metadata | GET | `/api/v2/odata/{Co}/BaqSvc/{BaqName}/$metadata` | Returns EDMX schema definition |
| Call EFx function | POST | `/api/v2/efx/{Co}/{Library}/{Function}/` | JSON body with function parameters [src1] |
| Get environment info | GET | `/api/v2/environment` | Returns accessible companies and plants [src1] |
| Get service metadata | GET | `/api/v2/odata/{Co}/Erp.BO.{Svc}/$metadata` | Full EDMX entity/method definitions |
| Get bearer token | POST | `/{Instance}/TokenResource.svc` | Basic Auth headers; returns JWT [src4] |
| Interactive API help | GET | `/{Instance}/api/help/v2/` | Swagger UI for exploring endpoints |

## Step-by-Step Integration Guide

### 1. Configure API Key in Epicor

Create an API Key in Epicor via System Setup > Security Maintenance > API Key Maintenance. Assign an Access Scope that defines which services and methods the key can access. Copy the generated API Key value. [src1]

```plaintext
Epicor Kinetic steps:
1. Navigate to System Setup > Security Maintenance > API Key Maintenance
2. Click "New" to create a new API Key
3. Set a description (e.g., "External Integration - Production")
4. Assign an Access Scope (defines accessible services/methods)
5. Save and copy the generated API Key value
```

**Verify**: In a browser, navigate to `https://{server}/{instance}/api/v2/odata/{Company}/Erp.BO.CompanySvc/Companies?api-key={YourKey}` -- you should see a JSON response (you'll need to be logged into the Epicor web client in the same browser session for this to work).

### 2. Obtain a Bearer Token

POST to the TokenResource.svc endpoint with Basic Auth credentials to receive a JWT bearer token for subsequent API calls. [src4]

```bash
# Input:  Epicor server URL, instance name, username, password
# Output: JWT bearer token

curl -s -X POST \
  "https://yourserver.com/EpicorInstance/TokenResource.svc" \
  -H "username: YourEpicorUser" \
  -H "password: YourPassword" \
  -H "Accept: application/json"

# Expected response:
# {
#   "AccessToken": "eyJhbGciOiJIUzI1NiIs...",
#   "ExpiresIn": 3600,
#   "TokenType": "Bearer"
# }
```

**Verify**: Decode the JWT at jwt.io to inspect claims (expiry, user, company). The `exp` claim should be in the future.

### 3. Query a Business Object with OData

Use the bearer token and API Key to query an Epicor Business Object with OData filtering and selection. [src1]

```bash
# Input:  Bearer token, API key, server URL
# Output: Filtered customer records as JSON

curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/Erp.BO.CustomerSvc/Customers?\$top=10&\$select=CustID,Name,City&\$filter=State%20eq%20'CA'" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "x-api-key: your-api-key-here" \
  -H "Accept: application/json"
```

**Verify**: Response contains `@odata.context` and a `value` array with matching customer records.

### 4. Query a BAQ (Business Activity Query)

Access a custom BAQ through the BaqSvc endpoint. BAQ parameters are passed as query string parameters. [src1]

```bash
# Input:  Bearer token, API key, BAQ name, optional parameters
# Output: BAQ result set as JSON

curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/BaqSvc/zCustomerOrders/Data?StartDate='2026-01-01'&EndDate='2026-03-01'" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "x-api-key: your-api-key-here" \
  -H "Accept: application/json"
```

**Verify**: Response contains `@odata.context` with BAQ-specific metadata and `value` array with query results.

### 5. Call an Epicor Function (EFx)

Invoke a custom Epicor Function via the EFx endpoint. Functions accept input parameters as JSON and return output parameters. [src1, src7]

```bash
# Input:  Bearer token, API key, function library and name, input parameters
# Output: Function output as JSON

curl -s -X POST \
  "https://yourserver.com/EpicorInstance/api/v2/efx/EPIC06/MyLibrary/CalculatePrice/" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "x-api-key: your-api-key-here" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"partNum": "WIDGET-001", "quantity": 100}'

# Expected response:
# {
#   "unitPrice": 12.50,
#   "totalPrice": 1250.00,
#   "discountApplied": true
# }
```

**Verify**: Response contains the expected output parameters with calculated values. A 404 indicates the function is not published (check if you need the `/staging/` path for unpublished functions).

### 6. Implement pagination for large result sets

Use $top and $skip OData parameters to paginate through large result sets that exceed the DefaultMaxRowCount limit. [src1]

```python
# Input:  Epicor connection details, BAQ or BO endpoint
# Output: All records via paginated requests

import requests

server = "https://yourserver.com/EpicorInstance"
company = "EPIC06"
api_key = "your-api-key"
token = "your-bearer-token"
page_size = 100
offset = 0
all_records = []

headers = {
    "Authorization": f"Bearer {token}",
    "x-api-key": api_key,
    "Accept": "application/json"
}

while True:
    url = (f"{server}/api/v2/odata/{company}/Erp.BO.PartSvc/Parts"
           f"?$top={page_size}&$skip={offset}&$select=PartNum,PartDescription")
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    data = response.json()
    records = data.get("value", [])
    if not records:
        break
    all_records.extend(records)
    offset += page_size

print(f"Retrieved {len(all_records)} total records")
```

**Verify**: `len(all_records)` matches the expected total count from Epicor.

## Code Examples

### Python: Query BAQ with authentication and pagination

```python
# Input:  Epicor server, instance, company, username, password, API key, BAQ name
# Output: All BAQ records with automatic pagination

import requests
from requests.auth import HTTPBasicAuth

server = "https://yourserver.com"
instance = "EpicorInstance"
company = "EPIC06"
api_key = "your-api-key"
username = "apiuser"
password = "apipassword"

# Step 1: Get bearer token
token_url = f"{server}/{instance}/TokenResource.svc"
token_resp = requests.post(
    token_url,
    headers={
        "username": username,
        "password": password,
        "Accept": "application/json"
    }
)
token_resp.raise_for_status()
access_token = token_resp.json()["AccessToken"]

# Step 2: Query BAQ with pagination
headers = {
    "Authorization": f"Bearer {access_token}",
    "x-api-key": api_key,
    "Accept": "application/json"
}
baq_name = "zOpenSalesOrders"
page_size = 100
offset = 0
all_rows = []

while True:
    url = (f"{server}/{instance}/api/v2/odata/{company}"
           f"/BaqSvc/{baq_name}/Data?$top={page_size}&$skip={offset}")
    resp = requests.get(url, headers=headers)
    resp.raise_for_status()
    rows = resp.json().get("value", [])
    if not rows:
        break
    all_rows.extend(rows)
    offset += page_size

print(f"Retrieved {len(all_rows)} records from BAQ '{baq_name}'")
```

### JavaScript/Node.js: Business Object CRUD operations

```javascript
// Input:  Epicor server details, API key, credentials
// Output: Customer record created and retrieved

// npm install node-fetch@2
const fetch = require("node-fetch");

const server = "https://yourserver.com/EpicorInstance";
const company = "EPIC06";
const apiKey = "your-api-key";

async function getToken(username, password) {
  const resp = await fetch(`${server}/TokenResource.svc`, {
    method: "POST",
    headers: {
      username,
      password,
      Accept: "application/json",
    },
  });
  const data = await resp.json();
  return data.AccessToken;
}

async function queryCustomers(token, filter) {
  const url = `${server}/api/v2/odata/${company}/Erp.BO.CustomerSvc/Customers`
    + `?$top=10&$select=CustID,Name,City,State`
    + `&$filter=${encodeURIComponent(filter)}`;

  const resp = await fetch(url, {
    headers: {
      Authorization: `Bearer ${token}`,
      "x-api-key": apiKey,
      Accept: "application/json",
    },
  });
  return resp.json();
}

(async () => {
  const token = await getToken("apiuser", "apipassword");
  const result = await queryCustomers(token, "State eq 'CA'");
  console.log(`Found ${result.value.length} customers in CA`);
  result.value.forEach((c) => console.log(`  ${c.CustID}: ${c.Name}`));
})();
```

### cURL: Quick API exploration

```bash
# Input:  Server URL, instance, company, API key, credentials
# Output: Various API responses for testing

# 1. Get bearer token
TOKEN=$(curl -s -X POST \
  "https://yourserver.com/EpicorInstance/TokenResource.svc" \
  -H "username: apiuser" \
  -H "password: apipassword" \
  -H "Accept: application/json" \
  | python -c "import sys,json; print(json.load(sys.stdin)['AccessToken'])")

# 2. List available companies
curl -s "https://yourserver.com/EpicorInstance/api/v2/environment" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-api-key: your-api-key" | python -m json.tool

# 3. Get Part metadata (schema discovery)
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/Erp.BO.PartSvc/\$metadata" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-api-key: your-api-key"

# 4. Query parts with OData filter
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/Erp.BO.PartSvc/Parts?\$top=5&\$select=PartNum,PartDescription&\$filter=TypeCode%20eq%20'M'" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-api-key: your-api-key" \
  -H "Accept: application/json" | python -m json.tool
```

## Data Mapping

### OData Query Options Reference

| OData Option | Syntax | Example | Notes |
|---|---|---|---|
| $select | Comma-delimited fields | `$select=CustID,Name,City` | Reduces payload; only specified fields returned |
| $filter | OData expressions | `$filter=State eq 'CA' and Active eq true` | Supports eq, ne, gt, ge, lt, le, and, or, not |
| $orderby | Field + direction | `$orderby=Name asc,CustNum desc` | asc (default) or desc |
| $top | Integer | `$top=50` | Max records to return; default max is 100 [src1] |
| $skip | Integer | `$skip=100` | Offset for pagination; combine with $top |
| $expand | Navigation property | `$expand=ShipToes` | Include child/related entities in response |
| $count | Boolean | `$count=true` | Returns total count of matching records |

[src1]

### $filter String Functions

| Function | Syntax | Example | Notes |
|---|---|---|---|
| contains | `contains(Field,'value')` | `$filter=contains(Name,'Steel')` | Case-sensitive substring match |
| startswith | `startswith(Field,'value')` | `$filter=startswith(CustID,'CUS')` | Prefix match |
| endswith | `endswith(Field,'value')` | `$filter=endswith(Name,'Inc')` | Suffix match |
| tolower | `tolower(Field)` | `$filter=tolower(Name) eq 'acme'` | Case-insensitive comparison |
| toupper | `toupper(Field)` | `$filter=toupper(State) eq 'CA'` | Case-insensitive comparison |
| substring | `substring(Field,start,length)` | `$filter=substring(CustID,0,3) eq 'CUS'` | Extract substring |
| length | `length(Field)` | `$filter=length(Name) gt 10` | String length comparison |
| trim | `trim(Field)` | `$filter=trim(Name) eq 'ACME'` | Remove whitespace |
| year/month/day | `year(Field)` | `$filter=year(OrderDate) eq 2026` | Date component extraction |

[src1]

### Data Type Gotchas

- **Composite keys require all segments**: Epicor Business Objects often use composite primary keys (e.g., `Company + CustNum` for Customer). When accessing by key, all key segments must be included in the parentheses: `Customers('EPIC06',123)`. Missing segments cause 404 errors. [src1]
- **Date filtering uses ISO 8601 format**: OData date filters must use `YYYY-MM-DD` format. Epicor-specific date fields may store time components -- use `date(OrderDate) eq 2026-01-15` for date-only comparison. [src1]
- **Boolean values are lowercase**: OData boolean values must be `true` or `false` (lowercase). Using `True`, `TRUE`, or `1` returns parse errors. [src1]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 Unauthorized | Invalid credentials | Username/password wrong, token expired, or missing auth header | Verify credentials; acquire fresh bearer token; check token ExpiresIn value [src4] |
| 403 Forbidden | Access denied: API Key | Missing or invalid API Key, or API Key scope doesn't include this service | Add `x-api-key` header or `?api-key=` query parameter; verify Access Scope in API Key Maintenance [src1, src2] |
| 404 Not Found | Service or entity not found | Wrong service name, BAQ name, or entity key; EFx function not published | Check exact service name in Swagger help; for EFx, try `/staging/` path for unpublished functions [src1] |
| 405 Method Not Allowed | Wrong HTTP method | Using GET on TokenResource.svc (requires POST); using POST on read-only BAQ | Use POST for token acquisition; use GET for read-only BAQ data queries [src4] |
| 500 Internal Server Error | Server-side exception | BPM directive error, business rule violation, or data validation failure | Check Epicor server event log; review BPM directive code; verify required fields in request body [src1] |
| Missing Root Element | Malformed JSON | Curly (smart) quotes instead of straight quotes in JSON body | Ensure JSON uses standard straight quotes (`"`) not typographic quotes [src7] |

[src1, src2, src4]

### Failure Points in Production

- **Bearer token expiry in long-running jobs**: Batch integrations running longer than the token lifetime (default 1h) fail mid-process with 401 errors. Fix: `Check token expiry before each batch of requests. If using Python, compare current time against the ExpiresIn value and acquire a new token proactively.` [src4]
- **Default 100-row limit truncates results silently**: OData queries return max 100 rows by default without any warning that more data exists. Fix: `Always implement pagination with $top + $skip. Check if the returned count equals $top value -- if so, there are likely more rows.` [src1, src5]
- **Session leak from missing Logout calls**: Each REST call with Basic Auth creates a server session. High-volume integrations without proper session cleanup exhaust server resources. Fix: `Use Bearer tokens instead of Basic Auth for high-volume operations. Call the Logout endpoint when done with a session.` [src1]
- **Application pool recycle clears sessions**: On-premise IIS application pool recycling (default every 29 hours) terminates all active sessions, causing in-flight requests to fail. Fix: `Implement retry logic for 401 errors that re-authenticates and retries the request.` [src6]
- **Smart quotes in JSON from copy-paste**: Copying JSON from Word or Outlook introduces curly quotes that cause "Missing Root Element" errors. Fix: `Always validate JSON with a linter before sending. Use a code editor, not a word processor, for JSON authoring.` [src7]
- **OData $expand on large child collections**: Expanding navigation properties with thousands of child records (e.g., all order lines for all orders) causes timeouts and memory issues. Fix: `Query parent and child entities separately with targeted $filter instead of using $expand on unbounded collections.` [src1]

## Anti-Patterns

### Wrong: Using Basic Auth for every request in high-volume integration

```python
# BAD -- sends username/password on every request, creates new session each time
for part_num in part_numbers:
    response = requests.get(
        f"{server}/api/v2/odata/{company}/Erp.BO.PartSvc/Parts('{company}','{part_num}')",
        auth=HTTPBasicAuth(username, password),
        headers={"x-api-key": api_key}
    )
```

### Correct: Acquire bearer token once, reuse for all requests

```python
# GOOD -- single authentication, reuse token for all requests
token = get_bearer_token(server, username, password)
headers = {"Authorization": f"Bearer {token}", "x-api-key": api_key}
session = requests.Session()
session.headers.update(headers)

for part_num in part_numbers:
    response = session.get(
        f"{server}/api/v2/odata/{company}/Erp.BO.PartSvc/Parts('{company}','{part_num}')"
    )
```

### Wrong: Fetching all records without pagination

```python
# BAD -- only gets first 100 records, silently misses the rest
response = requests.get(
    f"{server}/api/v2/odata/{company}/Erp.BO.CustomerSvc/Customers",
    headers=headers
)
customers = response.json()["value"]  # Only 100 customers!
```

### Correct: Paginate through all records

```python
# GOOD -- retrieves all records with explicit pagination
all_customers = []
offset = 0
page_size = 100

while True:
    response = requests.get(
        f"{server}/api/v2/odata/{company}/Erp.BO.CustomerSvc/Customers"
        f"?$top={page_size}&$skip={offset}",
        headers=headers
    )
    batch = response.json().get("value", [])
    if not batch:
        break
    all_customers.extend(batch)
    offset += page_size
```

### Wrong: Using v1 URL pattern for new integrations

```python
# BAD -- v1 pattern lacks Company scoping, no OData, deprecated
url = f"{server}/api/v1/Erp.BO.CustomerSvc/Customers"
```

### Correct: Use v2 URL pattern with Company in path

```python
# GOOD -- v2 pattern with OData, Company-scoped, API Key enforced
url = f"{server}/api/v2/odata/{company}/Erp.BO.CustomerSvc/Customers"
```

## Common Pitfalls

- **Forgetting Company in the v2 URL path**: v2 requires `/{Company}/` in the URL. Unlike v1 where Company was set via session headers, v2 explicitly scopes every request to a Company. Omitting it returns 404. Fix: `Always include the Company ID after /odata/ in the URL path.` [src1, src2]
- **Not URL-encoding $filter values**: OData filter expressions with special characters (spaces, ampersands, single quotes) must be URL-encoded. Single quotes inside string values must be doubled (`''`). Fix: `Use encodeURIComponent() or requests library's params argument to auto-encode.` [src1]
- **Assuming BAQ parameters work like OData $filter**: BAQ parameters are passed as top-level query string parameters (`?param1='value'`), not inside $filter. Putting BAQ parameters in $filter produces no results or errors. Fix: `Check the BAQ definition for parameter names and pass them as direct query string parameters.` [src1]
- **Using DELETE on records with dependencies**: Deleting a parent record (e.g., Customer) with active child records (e.g., Sales Orders) throws a business rule violation (500 error). Epicor enforces referential integrity at the business logic layer, not just the database. Fix: `Check for dependent records before deletion. Use GetByID to inspect relationships, or handle the 500 error gracefully with a user-facing message.` [src1]
- **Not recycling the app pool after API Key configuration changes**: On-premise installations may cache API Key configurations. New or modified API Keys may not take effect until the application pool is recycled. Fix: `Recycle the Epicor application pool in IIS after creating or modifying API Keys.` [src2]
- **Testing with browser session, deploying without auth**: The Epicor REST Help page works in a browser because the browser session is already authenticated. External applications must explicitly provide both auth credentials and API Key. Fix: `Always test with Postman or cURL using explicit headers before deploying.` [src2]

## Diagnostic Commands

```bash
# Check API connectivity and get available companies
curl -s "https://yourserver.com/EpicorInstance/api/v2/environment" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-api-key: $APIKEY" \
  -H "Accept: application/json" | python -m json.tool

# Verify authentication (simple BO query)
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/Erp.BO.CompanySvc/Companies?\$top=1" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-api-key: $APIKEY" \
  -H "Accept: application/json" | python -m json.tool

# List all available BAQs
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/BaqSvc" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-api-key: $APIKEY" \
  -H "Accept: application/json" | python -m json.tool

# Get BAQ metadata/schema
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/BaqSvc/YourBaqName/\$metadata" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-api-key: $APIKEY"

# Get BO service metadata (entity types, methods, relationships)
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/Erp.BO.PartSvc/\$metadata" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-api-key: $APIKEY"

# Test EFx function (staging/unpublished)
curl -s -X POST \
  "https://yourserver.com/EpicorInstance/api/v2/efx/staging/EPIC06/TestLib/TestFunc/" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-api-key: $APIKEY" \
  -H "Content-Type: application/json" \
  -d '{}' | python -m json.tool
```

## Version History & Compatibility

| API Version | Release | Status | Key Changes | Migration Notes |
|---|---|---|---|---|
| REST v2 (Kinetic 2024.x) | 2024-H1 | Current | EFx staging endpoint, Cloud SDK UD services | Use /staging/ for unpublished EFx during development |
| REST v2 (Kinetic 2022.2) | 2022-H2 | Supported | REST v2 became default; v1 deprecated for new development | All new integrations should use v2 URL pattern |
| REST v2 (10.2.700) | 2020 | Supported | v2 introduced: OData v4, Company in URL, API Key enforcement, JSON-only | Migrate v1 URLs: add /odata/{Company}/, add API Key header |
| REST v1 (10.2.x) | 2017-2020 | Deprecated | Original REST surface; no OData; XML/Atom supported | Replace /api/v1/ with /api/v2/odata/{Company}/; add API Key |
| WCF/SOAP | Legacy | Maintenance-only | Original service contracts | Migrate to REST v2; WCF services remain functional but unsupported for new development |

[src1, src2]

### Deprecation Policy

Epicor does not publish a formal API deprecation timeline. REST v1 and WCF services continue to function in current releases but receive no new features. Epicor's strategy is to converge on REST v2 + EFx as the sole integration surface. Cloud SDK (introduced 2023) generates REST-accessible UD services automatically, further cementing v2 as the standard. Monitor Epicor release notes and EpicWeb for deprecation notices. [src1]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Real-time CRUD operations on standard Epicor BOs (Customers, Parts, Orders) | Migrating >100K records in a single batch | ServerFileDownload endpoint for large exports; DMT (Data Migration Tool) for bulk imports |
| Exposing BAQ data to external BI tools, dashboards, or applications | Need offline/disconnected access to full Epicor dataset | Epicor Data Analytics (EDA) or GROW BI for analytics workloads |
| Invoking custom business logic via EFx functions | Modifying Epicor database directly | Always use REST API or BPM directives -- never write to Epicor DB directly |
| Integrating cloud/SaaS Epicor with third-party systems | Need sub-second response for high-frequency trading-style operations | Epicor is not designed for sub-millisecond latency; consider a caching layer |
| Building custom web/mobile applications against Epicor data | Need to generate Epicor SSRS reports programmatically | Use Ice.RPT service types with the REST API for report submission |

## Important Caveats

- Cloud/SaaS environments cannot modify server-side settings like DefaultMaxRowCount, IIS configurations, or application pool settings. Pagination is the only option for retrieving more than 100 rows per request. [src1, src5]
- API Key enforcement can be disabled via `EnforceApiKeyForRestApiV2=false`, but doing so removes all access scope controls and is a significant security risk. Only disable for specific cases like Excel OData connectivity where API Key headers cannot be configured. [src1]
- v1 and v2 REST endpoints coexist in all current Epicor releases. Legacy integrations using v1 will continue to work, but no new features or fixes are applied to v1. Plan migration to v2 for all active integrations. [src1, src2]
- Epicor does not document or enforce rate limits at the application level. High-volume integrations can overwhelm the server. Configure IIS Dynamic IP Security or a reverse proxy with rate limiting for production deployments. [src6]
- The interactive REST Help page (`/api/help/v2/`) is the most reliable source for available services, methods, and entity schemas in your specific Epicor version. The help page is auto-generated from the installed services and reflects your exact configuration. [src1]

## Related Units

- [Epicor Kinetic authentication methods](/business/erp-integration/epicor-kinetic-authentication/2026)
- [Epicor Prophet 21 API](/business/erp-integration/epicor-prophet-21-api/2026)
