TokenResource.svc (recommended for integrations), and Azure AD/Epicor IdP (recommended for SaaS with MFA). REST API v2 always requires an API key header.TokenResource.svc), or Azure AD / Epicor IdP (SaaS). API key required for all v2 endpoints.This card covers authentication, BPM customization, and Epicor Functions for Epicor Kinetic (the cloud-first rebranding of Epicor ERP 10). Kinetic runs both on-premise and as Epicor-hosted SaaS. All editions share the same REST API surface, but SaaS deployments restrict direct database access and require the Cloud SDK for custom objects. The REST API exposes every Business Object (BO), BAQ, and Function via OData-compatible endpoints. This card does NOT cover Epicor Prophet 21 (P21), Epicor for Retail, or Epicor BisTrack, which are separate product lines with different API surfaces.
| Property | Value |
|---|---|
| Vendor | Epicor |
| System | Epicor Kinetic (ERP 10+) |
| API Surface | REST (OData v4), SOAP (legacy) |
| Current API Version | v2 (introduced 2022.2; v1 still active for Kinetic UI) |
| Editions Covered | Cloud SaaS, On-Premise, Hybrid |
| Deployment | Hybrid (cloud + on-premise supported) |
| API Docs | Epicor Cloud SDK & REST Help (instance-specific) |
| Status | GA |
Epicor Kinetic exposes multiple API surfaces for different integration needs. REST API v2 is the primary surface for external integrations, while v1 remains active for the Kinetic browser UI.
| API Surface | Protocol | Best For | Auth Methods | Real-time? | Bulk? |
|---|---|---|---|---|---|
| REST API v2 | HTTPS/JSON (OData) | External integrations, Functions | Bearer token, Basic + API key | Yes | Limited |
| REST API v1 | HTTPS/JSON (OData) | Kinetic UI, legacy integrations | Bearer token, Basic | Yes | Limited |
| Epicor Functions (EFx) | HTTPS/JSON via v2 | Custom server-side logic, reusable business rules | Bearer token + API key | Yes | No |
| BAQ REST endpoints | HTTPS/JSON (OData) | Custom queries, reporting, read-heavy workloads | Bearer token, Basic + API key | Yes | No |
| SOAP/WCF Services | HTTPS/XML | Legacy integrations only | Basic, Windows auth | Yes | No |
| OData Feeds | OData v4 | Excel, Power BI, BI tools | Basic + API key | Yes | No |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max records per OData query | 100 default ($top) | All REST endpoints | Use $skip for pagination; configurable via server settings |
| Max request body size | Varies by server config | POST/PATCH operations | Default IIS limit applies (~28.6 MB for on-premise) |
| Max concurrent sessions | Server-dependent | All authenticated users | Controlled by Epicor license count |
| BAQ query timeout | 30 seconds default | BAQ REST endpoints | Configurable in BAQ designer |
| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| API calls | No hard published limit | N/A | SaaS: Epicor may throttle excessive usage; On-premise: limited by server capacity |
| Concurrent REST connections | License-dependent | Per-server | Each connection consumes a license seat unless using a dedicated integration account |
| TokenResource.svc requests | No explicit limit | N/A | Rate-limit token requests to avoid unnecessary load |
All Epicor Kinetic REST API v2 calls require an API key in addition to any authentication method. The API key acts as an application-level gate separate from user authentication.
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Basic Authentication | Quick testing, simple scripts, Swagger UI | N/A (per-request) | N/A | Credentials sent with every request (Base64). Only method for REST Help/Swagger UI. [src1] |
| Bearer Token (TokenResource.svc) | Server-to-server integrations, scheduled jobs | Default 1h (configurable up to 100h+) | No refresh token; request new token before expiry | POST to TokenResource.svc with username/password headers. Returns JWT. [src2, src3] |
| Azure AD / Epicor IdP | SaaS deployments, MFA-required environments | ~1h (Azure AD default) | Yes (Azure AD refresh tokens) | JWT obtained from Azure AD or Epicor IdP; passed as Bearer header. Recommended for production SaaS. [src1] |
| API Key (supplemental) | All v2 REST calls | Does not expire (until rotated) | N/A | Required in addition to any auth method above. Passed as header or query parameter. [src4] |
/apps/resthelp/) cannot use bearer tokens or API keys. Use Postman or custom code for testing token-based auth. [src1]x-api-key) or query parameter (?api-key={value}), even when using bearer token authentication./api/v2/efx/{Company}/{Library}/{Function}. V1 endpoints cannot invoke Functions.START -- Integrate with Epicor Kinetic
|-- What's the deployment model?
| |-- Cloud SaaS
| | |-- Authentication: Azure AD / Epicor IdP (recommended)
| | |-- Custom logic: Epicor Functions (EFx) via Cloud SDK
| | |-- Custom tables: UD Table Designer (no direct SQL)
| |-- On-Premise
| | |-- Authentication: Bearer token or Basic + API key
| | |-- Custom logic: Functions, BPM directives, or direct BO calls
| | |-- Custom tables: UD tables or direct SQL (if authorized)
| |-- Hybrid
| |-- Follow SaaS rules for Epicor-hosted components
| |-- Follow on-premise rules for local components
|-- What integration pattern?
| |-- Real-time CRUD (individual records)
| | |-- Use REST API v2 with Business Object endpoints
| | |-- Example: POST /api/v2/odata/{Company}/Erp.BO.SalesOrderSvc/SalesOrders
| |-- Event-driven (trigger on data change)
| | |-- Use BPM Data Directives to fire on insert/update/delete
| | |-- BPM calls Epicor Function which calls external REST endpoint
| |-- Custom server-side logic
| | |-- Create Epicor Function library
| | |-- Expose via REST: /api/v2/efx/{Company}/{Library}/{Function}
| | |-- Call from BPM directives, other Functions, or external REST
| |-- Batch/scheduled
| |-- Use BAQ REST endpoints for reads
| |-- Use BO endpoints with pagination for writes
| |-- Consider external orchestrator (Azure Logic Apps, n8n)
|-- Which API version?
| |-- v2 for all new external integrations (requires API key)
| |-- v1 only if legacy integration cannot add API key header
|-- Error handling?
|-- Implement token refresh before expiry
|-- Handle 401 (re-authenticate) and 429 (back off)
|-- Log all failures to external system (BPM error logging)
| Operation | Method | Endpoint Pattern | Auth Required | Notes |
|---|---|---|---|---|
| Get bearer token | POST | /{instance}/TokenResource.svc | username/password headers | Returns JWT with configurable lifetime |
| List Business Objects | GET | /api/v2/odata/{Company}/$metadata | Bearer + API key | OData metadata document |
| Query records (BO) | GET | /api/v2/odata/{Company}/Erp.BO.{Service}/{Entity} | Bearer + API key | Supports $filter, $select, $top, $skip |
| Create record | POST | /api/v2/odata/{Company}/Erp.BO.{Service}/{Entity} | Bearer + API key | JSON body matching entity schema |
| Update record | PATCH | /api/v2/odata/{Company}/Erp.BO.{Service}/{Entity}('{key}') | Bearer + API key | Partial update supported |
| Execute BAQ | GET | /api/v2/odata/{Company}/BaqSvc/{BaqName}/Data | Bearer + API key | Read-only; parameterized BAQs supported |
| Call Function | POST | /api/v2/efx/{Company}/{Library}/{Function} | Bearer + API key | JSON body with function parameters |
| List Function libraries | GET | /api/v2/efx/{Company} | Bearer + API key | Returns all published libraries |
| Call BO method | POST | /api/v2/odata/{Company}/Erp.BO.{Service}/{Method} | Bearer + API key | For non-CRUD operations (e.g., GetNewSalesOrder) |
Navigate to Epicor Admin Console > REST Settings > API Keys. Create a new key with a descriptive name. Copy the key value -- it is shown only once. [src4]
Admin Console Steps:
1. Open Epicor Admin Console (https://{server}/{instance}/admin)
2. Navigate to REST Settings > API Keys
3. Click "Add" to create new API key
4. Enter description: "External Integration - {System Name}"
5. Copy the generated API key value immediately
6. Optionally restrict the key to specific users/roles
Verify: GET /api/v2/odata/{Company}/Erp.BO.CompanySvc/Companies with the API key header returns company list.
POST to the TokenResource.svc endpoint with username and password headers. The response contains a JWT access token with configurable expiry (default 1 hour). [src2, src3]
curl -X POST "https://{server}/{instance}/TokenResource.svc" \
-H "username: epicor_integration_user" \
-H "password: YourSecurePassword" \
-H "Accept: application/json"
# Response:
# {
# "AccessToken": "eyJhbGciOiJIUzI1NiIs...",
# "ExpiresIn": 3600,
# "TokenType": "Bearer"
# }
Verify: Decode the JWT at jwt.io to confirm correct username and expiry claims.
Include both the Authorization header (Bearer token) and the API key header in every REST API v2 request. [src2, src4]
curl -X GET "https://{server}/{instance}/api/v2/odata/{Company}/Erp.BO.CustomerSvc/Customers?\$top=5&\$select=CustID,Name" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "x-api-key: your-api-key-here" \
-H "Accept: application/json"
Verify: Response returns HTTP 200 with JSON array of customer records. If 401: check both bearer token validity and API key.
Navigate to System Setup > Business Process Management > Method Directive Maintenance. Select the target BO and method, add a post-processing directive with custom C# code or Function call. [src5]
BPM Method Directive Setup:
1. Menu: System Setup > BPM > Method Directive Maintenance
2. Select BO: Erp.BO.SalesOrderSvc
3. Select Method: Update
4. Add Post-Processing Directive
5. Choose: "Execute Custom Code" or "Invoke Epicor Function"
6. Map input parameters from transaction context (ttSalesOrder)
7. Enable directive and save
Verify: Create or update a sales order. Check BPM logs for execution confirmation.
Create a Function Library in Application Studio, add a function with inputs/outputs, write C# logic, publish, then call via REST v2 EFx endpoint. [src6, src7]
Function Creation Steps:
1. Open Epicor Application Studio (or Function Maintenance)
2. Create new Function Library: "IntegrationLib"
3. Add Function: "SyncCustomerToExternal"
4. Define inputs: custID (string), company (string)
5. Define outputs: success (bool), message (string)
6. Write C# logic in the code editor
7. Publish the library
8. Call via REST: POST /api/v2/efx/{Company}/IntegrationLib/SyncCustomerToExternal
Verify: GET /api/v2/efx/{Company} lists your library. POST to function endpoint with test parameters and confirm response.
# Input: Epicor server URL, username, password, API key, company
# Output: List of sales orders from Epicor Kinetic
import requests
server = "https://your-epicor-server.com/EpicorInstance"
username = "integration_user"
password = "SecurePassword123"
api_key = "your-api-key-value"
company = "EPIC06"
# Step 1: Get bearer token
token_response = requests.post(
f"{server}/TokenResource.svc",
headers={
"username": username,
"password": password,
"Accept": "application/json"
}
)
token_data = token_response.json()
access_token = token_data["AccessToken"]
# Step 2: Query sales orders with bearer token + API key
headers = {
"Authorization": f"Bearer {access_token}",
"x-api-key": api_key,
"Accept": "application/json"
}
response = requests.get(
f"{server}/api/v2/odata/{company}/Erp.BO.SalesOrderSvc/SalesOrders",
headers=headers,
params={"$top": 10, "$select": "OrderNum,OrderDate,CustomerCustID"}
)
if response.status_code == 200:
orders = response.json().get("value", [])
for order in orders:
print(f"Order {order['OrderNum']}: {order['CustomerCustID']}")
else:
print(f"Error {response.status_code}: {response.text}")
// Input: Epicor server URL, credentials, API key, company, function params
// Output: Function execution result
// npm install node-fetch@2
const fetch = require("node-fetch");
const server = "https://your-epicor-server.com/EpicorInstance";
const company = "EPIC06";
const apiKey = process.env.EPICOR_API_KEY;
async function getToken() {
const resp = await fetch(`${server}/TokenResource.svc`, {
method: "POST",
headers: {
username: process.env.EPICOR_USER,
password: process.env.EPICOR_PASS,
Accept: "application/json",
},
});
const data = await resp.json();
return data.AccessToken;
}
async function callFunction(token, library, funcName, params) {
const resp = await fetch(
`${server}/api/v2/efx/${company}/${library}/${funcName}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"x-api-key": apiKey,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(params),
}
);
return resp.json();
}
(async () => {
const token = await getToken();
const result = await callFunction(token, "IntegrationLib", "SyncCustomer", {
custID: "CUST001",
company: "EPIC06",
});
console.log("Function result:", result);
})();
# Input: server URL, credentials, API key, company
# Output: Bearer token + customer records
# Step 1: Get bearer token
TOKEN=$(curl -s -X POST \
"https://server/instance/TokenResource.svc" \
-H "username: epicor_user" \
-H "password: YourPassword" \
-H "Accept: application/json" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['AccessToken'])")
# Step 2: Query customers with token + API key
curl -s "https://server/instance/api/v2/odata/EPIC06/Erp.BO.CustomerSvc/Customers?\$top=5" \
-H "Authorization: Bearer $TOKEN" \
-H "x-api-key: your-api-key" \
-H "Accept: application/json" | python3 -m json.tool
# Step 3: Call an Epicor Function
curl -s -X POST \
"https://server/instance/api/v2/efx/EPIC06/MyLibrary/MyFunction" \
-H "Authorization: Bearer $TOKEN" \
-H "x-api-key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"param1": "value1", "param2": "value2"}' | python3 -m json.tool
| Resource Type | v1 Endpoint | v2 Endpoint | Key Difference |
|---|---|---|---|
| Business Object CRUD | /api/v1/Erp.BO.{Svc}/{Entity} | /api/v2/odata/{Company}/Erp.BO.{Svc}/{Entity} | v2 requires Company in path + API key |
| BAQ Data | /api/v1/BaqSvc/{BaqName} | /api/v2/odata/{Company}/BaqSvc/{BaqName}/Data | v2 adds /Data suffix |
| Epicor Functions | N/A | /api/v2/efx/{Company}/{Library}/{Function} | Functions are v2 only |
| BO Methods | /api/v1/Erp.BO.{Svc}/{Method} | /api/v2/odata/{Company}/Erp.BO.{Svc}/{Method} | Same pattern, v2 adds Company |
| OData Metadata | /api/v1/$metadata | /api/v2/odata/{Company}/$metadata | v2 is Company-scoped |
2026-03-02T00:00:00 without timezone offset. Treat Epicor dates as server-local timezone unless explicitly specified. [src8]true/false, not integer equivalents. Sending 1 instead of true may cause 400 Bad Request. [src8]$metadata for Precision and Scale attributes. [src8]| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 Unauthorized | Authentication failed | Expired token, missing API key, invalid credentials | Check both bearer token validity AND API key header. Re-authenticate if token expired. |
| 403 Forbidden | Authorization failed | User lacks permission for the requested BO/method | Check Epicor security group and menu security for the integration user. |
| 404 Not Found | Endpoint or record not found | Wrong BO name, incorrect API version path, nonexistent record key | Verify endpoint against REST Help page. Check v1 vs v2 path format. |
| 400 Bad Request | Invalid payload | Malformed JSON, wrong field types, missing required fields | Check $metadata for field requirements. Validate JSON before sending. |
| 500 Internal Server Error | Server-side error | BPM custom code exception, database constraint violation | Check Epicor server event log. Review directive code for unhandled exceptions. |
| 503 Service Unavailable | Server overloaded or maintenance | Too many concurrent connections or scheduled maintenance | Implement retry with exponential backoff. Check server status. |
Check token expiry before each batch. Re-authenticate proactively when >80% of lifetime has elapsed. [src3]Maintain API key in a configuration store (env var, vault). Implement key rotation process that updates all consumers. [src4]Wrap all BPM custom code in try/catch. Log errors to a UD table or external service. [src5]Use a dedicated integration user. Implement connection pooling. Close sessions explicitly. [src8]Test all customizations in pilot environment before Epicor applies update to production. [src7]# BAD -- credentials transmitted on every request, no caching
for order_num in order_numbers:
response = requests.get(
f"{server}/api/v2/odata/{company}/Erp.BO.SalesOrderSvc/SalesOrders('{order_num}')",
auth=("username", "password"), # credentials sent every time
headers={"x-api-key": api_key}
)
# GOOD -- authenticate once, reuse token for all requests
import time
token_data = get_token(server, username, password)
token = token_data["AccessToken"]
token_expiry = time.time() + token_data["ExpiresIn"] - 300 # 5 min buffer
for order_num in order_numbers:
if time.time() > token_expiry:
token_data = get_token(server, username, password)
token = token_data["AccessToken"]
token_expiry = time.time() + token_data["ExpiresIn"] - 300
response = requests.get(
f"{server}/api/v2/odata/{company}/Erp.BO.SalesOrderSvc/SalesOrders('{order_num}')",
headers={"Authorization": f"Bearer {token}", "x-api-key": api_key}
)
// BAD -- complex integration logic embedded in BPM directive
// Hard to test, hard to reuse, hard to debug
var client = new System.Net.Http.HttpClient();
var json = Newtonsoft.Json.JsonConvert.SerializeObject(ttSalesOrder);
var response = client.PostAsync("https://external-system.com/api",
new StringContent(json, Encoding.UTF8, "application/json")).Result;
// No error handling, blocks the transaction, not reusable
// GOOD -- BPM directive calls a Function; logic is in the Function
// Function can be tested independently, reused, called via REST
CallService<Ice.Contracts.EfxSvcContract>(efxSvc =>
{
var request = new EfxCallRequest("IntegrationLib", "SyncOrderToExternal");
request.Parameters.Add("orderNum", ttSalesOrder.FirstOrDefault()?.OrderNum);
var result = efxSvc.Execute(request);
});
# BAD -- v1 lacks API key security, no access to Functions
curl "https://server/instance/api/v1/Erp.BO.CustomerSvc/Customers" \
-H "Authorization: Basic dXNlcjpwYXNz"
# No API key = no application-level access control
# GOOD -- v2 with API key provides application-level security
curl "https://server/instance/api/v2/odata/EPIC06/Erp.BO.CustomerSvc/Customers" \
-H "Authorization: Bearer eyJhbGci..." \
-H "x-api-key: your-api-key-here"
# API key can be scoped, rotated, and revoked independently
Always include company code in v2 paths. Check REST Help for exact format. [src4]Use OData-aware libraries or manually escape special characters. [src8]Create and test with the actual integration user account. [src8]Document all BPM directives per method. Use sequence numbers deliberately. [src5]Implement idempotent operations. Track progress externally. [src8]Store configuration in Epicor User Codes. Read dynamically in Function code. [src5]# Get bearer token and verify it works
TOKEN=$(curl -s -X POST "https://server/instance/TokenResource.svc" \
-H "username: your_user" -H "password: your_pass" \
-H "Accept: application/json" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['AccessToken'])")
# Decode JWT to check claims and expiry
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# Test authentication + API key with simple endpoint
curl -s "https://server/instance/api/v2/odata/EPIC06/Erp.BO.CompanySvc/Companies" \
-H "Authorization: Bearer $TOKEN" \
-H "x-api-key: your-api-key" \
-H "Accept: application/json" | python3 -m json.tool
# List available Epicor Function libraries
curl -s "https://server/instance/api/v2/efx/EPIC06" \
-H "Authorization: Bearer $TOKEN" \
-H "x-api-key: your-api-key" \
-H "Accept: application/json" | python3 -m json.tool
# Check OData metadata for a specific Business Object
curl -s "https://server/instance/api/v2/odata/EPIC06/Erp.BO.SalesOrderSvc/\$metadata" \
-H "Authorization: Bearer $TOKEN" \
-H "x-api-key: your-api-key" \
-H "Accept: application/xml"
# Test a BAQ endpoint
curl -s "https://server/instance/api/v2/odata/EPIC06/BaqSvc/zCustomerList/Data?\$top=5" \
-H "Authorization: Bearer $TOKEN" \
-H "x-api-key: your-api-key" \
-H "Accept: application/json" | python3 -m json.tool
| API Version | Release | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| REST v2 | 2022.2 | Current (recommended) | Mandatory API key; Company in URL path | Add API key header; add Company to URL paths |
| REST v1 | 10.0 (2014) | Supported (UI dependency) | None | Cannot access Functions; no API key security; still used by Kinetic UI |
| SOAP/WCF | 10.0 (2014) | Maintenance only | Deprecated for new development | Migrate to REST v2; SOAP may be removed in future |
| Epicor Functions (EFx) | 2021.1 | GA | N/A (new feature) | Requires v2 endpoints; replaces many BPM custom code patterns |
| Cloud SDK (UD Services) | 2023.x | GA | N/A (new feature) | SaaS-only; generates REST endpoints, tables, and UI automatically |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building external integrations with Epicor Kinetic (orders, inventory, customers) | Integrating with Epicor Prophet 21 (P21) | P21 has its own REST API with different auth |
| Need event-driven triggers on Epicor data changes (BPM directives) | Need real-time streaming/CDC from Epicor | Consider database-level CDC if on-premise; no native streaming API |
| Creating reusable server-side business logic (Epicor Functions) | Need sub-second latency for high-frequency systems | Epicor REST is not designed for ultra-low-latency use cases |
| SaaS deployment needing custom tables and logic (Cloud SDK) | Need direct SQL access to Epicor database | Only possible on-premise; SaaS prohibits direct DB access |
| Bearer token auth for server-to-server integration | Need interactive SSO with MFA for end users | Use Azure AD / Epicor IdP for interactive auth flows |
/apps/resthelp/) is a valuable discovery tool but only supports Basic authentication. You cannot test bearer tokens or API keys there.