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 | Epicor REST Help (per-instance Swagger) |
| Status | GA |
Epicor Kinetic exposes four distinct service types through the REST API v2, but only Business Objects (BO) and BAQ Services 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 | Full ($select, $filter, $expand, $orderby, $top, $skip) | Yes | /api/v2/odata/{Co}/Erp.BO.{Svc}/{EntitySet} |
| BAQ Services (BaqSvc) | HTTPS/JSON (OData v4) | Custom queries, reporting | Full ($select, $filter, $orderby, $top, $skip) | Yes | /api/v2/odata/{Co}/BaqSvc/{BaqName}/Data |
| Epicor Functions (EFx) | HTTPS/JSON (RPC) | Custom serverless logic | No (POST only) | Yes | /api/v2/efx/{Co}/{Library}/{Function}/ |
| Library/Process/Report | HTTPS/JSON (RPC) | Custom methods, batch processes | No (custom methods only) | Varies | /api/v2/odata/{Co}/Ice.{Type}.{Svc}/{Method} |
| 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 | HTTP response body | Practical limit; large responses may timeout [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 processed independently |
| Limit Type | Value | Window | Notes |
|---|---|---|---|
| API rate limiting | None built-in | N/A | Must configure at IIS or reverse proxy [src6] |
| Concurrent connections | Server-dependent | Per-IIS instance | Controlled by IIS application pool settings [src6] |
| Web Services licensing | License-based throttling | Per-session | Epicor progressively doubles response delay when WS licenses exhausted [src6] |
| Session timeout | Configurable | Per-session | Orphaned sessions consume server resources |
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. [src1, src2]
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Basic Auth + API Key | Simple integrations, testing | Per-request | N/A | Credentials sent every request [src1] |
| Bearer Token + API Key | Production integrations | Default ~1h, configurable | No refresh; request new before expiry | POST to TokenResource.svc [src4] |
| Azure AD + API Key | Cloud/SaaS enterprise SSO | Azure AD token lifetime | Yes (via Azure AD) | Cloud only [src3] |
| Epicor IdP + API Key | Cloud/SaaS with Epicor identity | IdP token lifetime | Yes (via IdP) | Cloud only; MFA support [src3] |
| Windows SSO + API Key | On-premise Active Directory | Windows session | N/A | On-premise only [src1] |
START -- Integrate with Epicor Kinetic REST API v2
|-- What service type do you need?
| |-- Standard ERP entity CRUD (Customers, Parts, Orders)
| | --> Business Object (BO) endpoint with OData
| | |-- Read single record? --> GET /{Co}/Erp.BO.{Svc}/{Entity}('{Key}')
| | |-- Read with filters? --> GET with $filter, $select, $top, $skip
| | |-- Create/Update/Delete? --> POST / PATCH / DELETE
| | |-- Custom method? --> POST /{Co}/Erp.BO.{Svc}/{Method}
| |-- Custom query / reporting?
| | --> BAQ Service (BaqSvc) endpoint
| | |-- Simple read? --> GET /{Co}/BaqSvc/{BAQ}/Data
| | |-- Parameterized? --> GET .../{BAQ}/Data?param1='value1'
| | |-- Need to write back? --> Updatable BAQ with PATCH
| |-- Custom logic / serverless function?
| | --> EFx endpoint: POST /efx/{Co}/{Library}/{Function}/
| |-- Batch process or report?
| --> Library/Process/Report service (RPC only)
|-- What volume?
| |-- < 100 records --> Single OData query
| |-- 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
| |-- High-volume batch --> Bearer Token (reduce credential exchange)
| Operation | Method | Endpoint Pattern | Notes |
|---|---|---|---|
| List BO records | GET | /api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers | Default 100 rows |
| Get single record | GET | /api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers('{CustID}',{CustNum}) | Composite key |
| Create record | POST | /api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers | JSON body |
| Update record | PATCH | /api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers('{CustID}',{CustNum}) | Changed fields only |
| Delete record | DELETE | /api/v2/odata/{Co}/Erp.BO.CustomerSvc/Customers('{CustID}',{CustNum}) | 204 on success |
| Invoke BO method | POST | /api/v2/odata/{Co}/Erp.BO.CustomerSvc/GetByID | JSON params |
| Query BAQ | GET | /api/v2/odata/{Co}/BaqSvc/{BaqName}/Data | OData options apply |
| BAQ with parameters | GET | /api/v2/odata/{Co}/BaqSvc/{BaqName}/Data?param1='val' | Query string params |
| List all BAQs | GET | /api/v2/odata/{Co}/BaqSvc | Service document |
| BAQ metadata | GET | /api/v2/odata/{Co}/BaqSvc/{BaqName}/$metadata | EDMX schema |
| Call EFx function | POST | /api/v2/efx/{Co}/{Library}/{Function}/ | JSON body params |
| Environment info | GET | /api/v2/environment | Companies and plants |
| Service metadata | GET | /api/v2/odata/{Co}/Erp.BO.{Svc}/$metadata | Full EDMX |
| Get bearer token | POST | /{Instance}/TokenResource.svc | Basic Auth headers |
Create an API Key via System Setup > Security Maintenance > API Key Maintenance. Assign an Access Scope and copy the generated key. [src1]
Epicor Kinetic steps:
1. System Setup > Security Maintenance > API Key Maintenance
2. Click "New" to create a new API Key
3. Set description and assign an Access Scope
4. Save and copy the generated API Key value
Verify: Navigate to https://{server}/{instance}/api/v2/odata/{Company}/Erp.BO.CompanySvc/Companies?api-key={YourKey} in a browser with active Epicor session.
POST to TokenResource.svc with Basic Auth credentials to receive a JWT. [src4]
curl -s -X POST \
"https://yourserver.com/EpicorInstance/TokenResource.svc" \
-H "username: YourEpicorUser" \
-H "password: YourPassword" \
-H "Accept: application/json"
# Response: {"AccessToken":"eyJ...","ExpiresIn":3600,"TokenType":"Bearer"}
Verify: Decode JWT at jwt.io; exp claim should be in the future.
Use bearer token and API Key to query with OData filtering. [src1]
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 eyJ..." \
-H "x-api-key: your-api-key" \
-H "Accept: application/json"
Verify: Response contains @odata.context and value array with matching records.
Access a BAQ via BaqSvc. Parameters are passed as query strings. [src1]
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/BaqSvc/zCustomerOrders/Data?StartDate='2026-01-01'" \
-H "Authorization: Bearer eyJ..." \
-H "x-api-key: your-api-key" \
-H "Accept: application/json"
Verify: Response contains BAQ-specific metadata and value array.
Invoke custom logic via POST to the EFx endpoint. [src1, src7]
curl -s -X POST \
"https://yourserver.com/EpicorInstance/api/v2/efx/EPIC06/MyLibrary/CalculatePrice/" \
-H "Authorization: Bearer eyJ..." \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"partNum": "WIDGET-001", "quantity": 100}'
Verify: Response returns output parameters. 404 means function not published (try /staging/ path).
Use $top and $skip to paginate beyond the DefaultMaxRowCount limit. [src1]
import requests
page_size = 100
offset = 0
all_records = []
while True:
url = f"{server}/api/v2/odata/{company}/Erp.BO.PartSvc/Parts?$top={page_size}&$skip={offset}"
response = requests.get(url, headers=headers)
records = response.json().get("value", [])
if not records:
break
all_records.extend(records)
offset += page_size
Verify: len(all_records) matches expected total from Epicor.
# Input: Epicor server details, API key, credentials, BAQ name
# Output: All BAQ records with automatic pagination
import requests
server = "https://yourserver.com"
instance = "EpicorInstance"
company = "EPIC06"
api_key = "your-api-key"
# Get bearer token
token_resp = requests.post(
f"{server}/{instance}/TokenResource.svc",
headers={"username": "apiuser", "password": "apipass", "Accept": "application/json"}
)
access_token = token_resp.json()["AccessToken"]
headers = {
"Authorization": f"Bearer {access_token}",
"x-api-key": api_key,
"Accept": "application/json"
}
baq_name = "zOpenSalesOrders"
page_size, offset, all_rows = 100, 0, []
while True:
url = f"{server}/{instance}/api/v2/odata/{company}/BaqSvc/{baq_name}/Data?$top={page_size}&$skip={offset}"
rows = requests.get(url, headers=headers).json().get("value", [])
if not rows:
break
all_rows.extend(rows)
offset += page_size
print(f"Retrieved {len(all_rows)} records")
// Input: Epicor server details, API key, credentials
// Output: Filtered customer records
const fetch = require("node-fetch");
const server = "https://yourserver.com/EpicorInstance";
const company = "EPIC06";
const apiKey = "your-api-key";
async function getToken(user, pass) {
const resp = await fetch(`${server}/TokenResource.svc`, {
method: "POST",
headers: { username: user, password: pass, Accept: "application/json" }
});
return (await resp.json()).AccessToken;
}
async function queryCustomers(token, filter) {
const url = `${server}/api/v2/odata/${company}/Erp.BO.CustomerSvc/Customers`
+ `?$top=10&$select=CustID,Name,City&$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", "apipass");
const result = await queryCustomers(token, "State eq 'CA'");
console.log(`Found ${result.value.length} customers`);
})();
# Get bearer token
TOKEN=$(curl -s -X POST "https://yourserver.com/EpicorInstance/TokenResource.svc" \
-H "username: apiuser" -H "password: apipass" -H "Accept: application/json" \
| python -c "import sys,json; print(json.load(sys.stdin)['AccessToken'])")
# List available companies
curl -s "https://yourserver.com/EpicorInstance/api/v2/environment" \
-H "Authorization: Bearer $TOKEN" -H "x-api-key: your-key" | python -m json.tool
# Query parts with OData filter
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/Erp.BO.PartSvc/Parts?\$top=5&\$select=PartNum,PartDescription" \
-H "Authorization: Bearer $TOKEN" -H "x-api-key: your-key" -H "Accept: application/json" | python -m json.tool
| OData Option | Syntax | Example | Notes |
|---|---|---|---|
| $select | Comma-delimited | $select=CustID,Name,City | Reduces payload size |
| $filter | OData expressions | $filter=State eq 'CA' | eq, ne, gt, ge, lt, le, and, or, not |
| $orderby | Field + direction | $orderby=Name asc | asc (default) or desc |
| $top | Integer | $top=50 | Max rows; default limit is 100 |
| $skip | Integer | $skip=100 | Offset for pagination |
| $expand | Navigation property | $expand=ShipToes | Include child entities |
| $count | Boolean | $count=true | Returns total count |
| Function | Syntax | Example |
|---|---|---|
| contains | contains(Field,'value') | $filter=contains(Name,'Steel') |
| startswith | startswith(Field,'value') | $filter=startswith(CustID,'CUS') |
| endswith | endswith(Field,'value') | $filter=endswith(Name,'Inc') |
| tolower | tolower(Field) | $filter=tolower(Name) eq 'acme' |
| year/month/day | year(Field) | $filter=year(OrderDate) eq 2026 |
| trim | trim(Field) | $filter=trim(Name) eq 'ACME' |
| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 | Unauthorized | Wrong credentials or expired token | Verify credentials; acquire fresh token [src4] |
| 403 | Access denied: API Key | Missing or invalid API Key | Add x-api-key header; check Access Scope [src1, src2] |
| 404 | Not Found | Wrong service, BAQ, or entity key name | Check name in Swagger help; try /staging/ for EFx [src1] |
| 405 | Method Not Allowed | Wrong HTTP verb (e.g., GET on TokenResource.svc) | Use POST for tokens; GET for read-only BAQs [src4] |
| 500 | Internal Server Error | BPM error, validation failure | Check server event log; review BPM code [src1] |
| Missing Root Element | Malformed JSON | Smart/curly quotes in JSON body | Use straight quotes; validate JSON [src7] |
Check token expiry before each batch; acquire new token proactively. [src4]Always implement pagination with $top + $skip. [src1, src5]Use Bearer tokens; call Logout when done. [src1]Implement retry logic for 401 errors that re-authenticates. [src6]Validate JSON with a linter; use code editors, not word processors. [src7]Query parent and child entities separately with targeted $filter. [src1]# BAD -- sends credentials every request, creates new session each time
for part_num in part_numbers:
response = requests.get(url, auth=HTTPBasicAuth(user, pwd), headers={"x-api-key": key})
# GOOD -- single auth, reuse token and session
token = get_bearer_token(server, user, pwd)
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {token}", "x-api-key": key})
for part_num in part_numbers:
response = session.get(url)
# BAD -- only gets first 100 records, silently misses the rest
customers = requests.get(f"{server}/.../Customers", headers=h).json()["value"]
# GOOD -- retrieves all records with explicit pagination
all_data, offset = [], 0
while True:
batch = requests.get(f"{url}?$top=100&$skip={offset}", headers=h).json().get("value", [])
if not batch: break
all_data.extend(batch)
offset += 100
# BAD -- v1 lacks Company scoping, no OData, deprecated
url = f"{server}/api/v1/Erp.BO.CustomerSvc/Customers"
# GOOD -- v2 with OData, Company-scoped, API Key enforced
url = f"{server}/api/v2/odata/{company}/Erp.BO.CustomerSvc/Customers"
/{Company}/ after /odata/. Omitting returns 404. Fix: Always include Company ID in URL path. [src1, src2]Use encodeURIComponent() or requests library params. [src1]Pass BAQ params as direct query string: ?param1='value'. [src1]Check dependent records before deletion. [src1]Recycle IIS application pool after API Key modifications. [src2]Always test with Postman or cURL using explicit credentials. [src2]# Check API connectivity and list companies
curl -s "https://yourserver.com/EpicorInstance/api/v2/environment" \
-H "Authorization: Bearer $TOKEN" -H "x-api-key: $APIKEY" | python -m json.tool
# Verify authentication
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" | 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" | python -m json.tool
# Get BAQ metadata/schema
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/BaqSvc/YourBaq/\$metadata" \
-H "Authorization: Bearer $TOKEN" -H "x-api-key: $APIKEY"
# Get BO service metadata
curl -s "https://yourserver.com/EpicorInstance/api/v2/odata/EPIC06/Erp.BO.PartSvc/\$metadata" \
-H "Authorization: Bearer $TOKEN" -H "x-api-key: $APIKEY"
| 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 |
| REST v2 (Kinetic 2022.2) | 2022-H2 | Supported | v2 became default; v1 deprecated | All new integrations should use v2 |
| REST v2 (10.2.700) | 2020 | Supported | OData v4, Company in URL, API Key, JSON-only | Add /odata/{Company}/ and API Key |
| REST v1 (10.2.x) | 2017-2020 | Deprecated | Original REST; XML/Atom supported | Replace /api/v1/ with /api/v2/odata/{Co}/ |
| WCF/SOAP | Legacy | Maintenance-only | Original service contracts | Migrate to REST v2 |
Epicor does not publish a formal API deprecation timeline. REST v1 and WCF services continue to function but receive no new features. Epicor's strategy is REST v2 + EFx as the sole integration surface. Monitor Epicor release notes and EpicWeb for deprecation notices. [src1]
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Real-time CRUD on standard Epicor BOs | Migrating >100K records in a single batch | ServerFileDownload or DMT (Data Migration Tool) |
| Exposing BAQ data to BI/dashboards | Need offline access to full Epicor dataset | Epicor Data Analytics (EDA) or GROW BI |
| Custom logic via EFx functions | Direct database modifications | Always use REST API or BPM -- never write to DB directly |
| Integrating cloud Epicor with third-party systems | Sub-millisecond latency requirements | Add a caching layer in front of Epicor API |
| Building custom web/mobile apps | Generating SSRS reports programmatically | Ice.RPT service types via REST API |
/api/help/v2/) is the most reliable source for your specific version's available services and methods. [src1]