Find it: Management → API & Integrations (Admin group) → Epicor. Each integration opens to the tabs Overview, Connection, Endpoints & Payloads, Field Mapping, Sync History and Failed Records.
There are two ways OpexMX works with Epicor Kinetic, and they do different jobs.
- Native sync. A built-in sync engine that reads and writes Epicor business objects over the Kinetic REST API, on a schedule, for four record types.
- OpexFlow for postings. For transactions that must reach the ERP with proof of delivery, OpexMX sends a signed webhook to OpexFlow, which delivers it, confirms it and flags what did not arrive.
This page describes the native sync from the product code, then the OpexFlow path.
Status of this page
The native sync is described as product capability, read from the OpexMX server code. This page does not report a test run of the native sync, and no results for it are claimed here.
Recorded tests exist for the OpexFlow path. They are published as OpexFlow case studies, linked at the end. Each states what was simulated and what was real.
Native sync
What syncs
| OpexMX record | Epicor business object and key |
|---|---|
| Maintenance ticket (work order) | Erp.BO.JobEntrySvc / JobEntries, key JobNum |
| Asset | Erp.BO.EquipSvc / Equips, key EquipID (with Plant) |
| Part | Erp.BO.PartSvc / Parts, key PartNum |
| Purchase order, with lines | Erp.BO.POSvc / POes, key PONum (numeric), lines expanded from PODetails |
Each of the four can be configured as inbound (Epicor to OpexMX), outbound (OpexMX to Epicor) or bidirectional. The direction is set per entity in the integration configuration. Only the entities listed in the configuration are synced. No other Epicor objects are read or written.
The entity set names and keys in the code are annotated as checked against the Kinetic REST v2 $metadata of EPIC06, Epicor's demo company. That is a schema check, not a sync test.
Default field mapping
Each entity ships with a default mapping from the Epicor field to the OpexMX field. A per-entity override can be added in the integration configuration, and the Field Mapping tab shows the defaults.
| Entity | Epicor field | OpexMX field |
|---|---|---|
| Work order | JobNum | externalId |
PartDescription | title | |
CommentText | description | |
StartDate / DueDate | scheduledStartDate / scheduledEndDate | |
| Asset | EquipID | code |
Description | name | |
TypeID | assetType | |
OEM, Model, SerialNum | manufacturer, model, serialNumber | |
Plant | location | |
| Part | PartNum | code |
PartDescription | name | |
IUM | unit | |
ProdCode | category | |
| Purchase order | PONum | externalId |
VendorNum | supplierCode | |
VendorName | supplierName | |
OrderDate | orderDate |
Some values are derived rather than copied:
- Work order status comes from the Epicor job flags: closed, then complete, then released (
in_progress), and a job that is not released isopen. Going the other way, the OpexMX status sets the same flags. A job of typeMNTbecomes a preventive ticket, any other job type a corrective ticket. - Asset status comes from
InActive: active or inactive. - Purchase order status comes from
OpenOrder:issuedwhen open,closedwhen not. Outbound, a status ofclosedorreceivedclearsOpenOrder. - Purchase order lines map
POLine,PartNum,LineDesc,OrderQty,IUMandDocUnitCostto line number, part code, description, quantity, unit and unit price.
How records are matched
Inbound records are matched to an existing OpexMX record by their Epicor key, then updated, or created if none matches:
| Entity | Matched on |
|---|---|
| Work order | externalId (the job number) |
| Asset | code (the equipment ID) |
| Part | code (the part number) |
| Purchase order | PO number |
Outbound, the engine looks the record up in Epicor by its key with a $filter. If it exists the engine sends a PATCH to the keyed entity URL (company and key fields in the address). If it does not exist, or if the lookup itself fails after two attempts, the engine sends a POST to create it. Payloads are plain entity JSON for Kinetic REST v2.
Connection and authentication
The connection is stored with the integration and holds:
- the base URL of the Epicor application server, the instance name and the company ID, which also appears in the request path
- the REST API version,
v2orv3, withv3used when none is set - the API key, sent as the
x-api-keyheader, required for v2 and later - the authentication type: basic (username and password) or bearer (a token)
- a request timeout, 30 seconds by default
- an optional retry policy (see Retry below)
Requests go to /api/{version}/odata/{company}/Erp.BO.{service}/{entity}, over OData v4.
The credentials are encrypted when stored. The Connection tab shows only the host, company, API version and whether an API key and credentials are set. Secrets are never returned to the browser. A trailing slash or stray whitespace in the base URL is cleaned before use.
Test connection requests one part ($top=1) with a 10-second timeout. It returns success or failure with the latency. A 401 is reported as an authentication failure, not as a reachable system. Epicor's own error text, for example an invalid API key message, is passed through so the cause is visible in the screen.
Schedule
- A scheduler tick runs every 15 minutes and looks at every active integration.
- An integration is due when it has never synced, or when the time since its last sync is at least its
scheduleMinutessetting. The default is 15 minutes. Because the tick is 15 minutes, a shorter setting still runs at most once per tick. - On Cloudflare, each integration has its own Durable Object. It syncs in chunks across alarm ticks so a long run survives eviction and time limits. On-premise installs run the same sync inline in the server process.
- A separate health probe runs every 30 minutes and calls the connection test for every active integration, so a broken connection shows up even between syncs.
- Users can also start a sync by hand, retry a single entity, or test the connection from the integration screen.
Incremental sync and paging
Records move in pages of 25. Inbound requests use $top and $skip. Outbound reads the changed OpexMX rows, 25 at a time, ordered by id.
Each integration, entity and direction has a watermark, the time of the last complete pass:
- Inbound: if the entity has a delta field configured (for example
LastActivityDate), the request adds$filter=<field> ge <watermark>. The first run has no watermark, so it is a full backfill. With no delta field set, every record is read each time. - Outbound: rows changed since the watermark are sent. With no watermark the engine looks back 24 hours.
- The watermark moves forward only when the last page of that pass was short, meaning the entity has drained. If a fetch fails, the watermark does not move and the next run retries the same window.
Retry
Calls to Epicor are wrapped in a retry with exponential backoff and jitter:
- default 3 attempts, starting at 1 second with a multiplier of 2, capped at 30 seconds per wait
- the connection retry policy can change the attempts, the initial delay and the multiplier
- retried: network errors, timeouts, and HTTP
408,425,429,500,502,503,504 - not retried: other
4xxanswers, including401, which fail at once
Dead letters
A record that cannot be mapped, saved or sent does not stop the run. The failure is stored with the record's payload and the error text in a dead-letter table, and the sync continues with the next record.
- Each stored failure is replayed automatically after a run. Replay waits 1 minute, then 2, then 4, and so on, up to 30 minutes between attempts.
- After 5 attempts the failure is marked dead and stops being replayed automatically.
- Inbound failures are replayed from the mapped record. Outbound failures are mapped and sent again.
- A failed page fetch is not a dead letter. It is logged as an error on the sync log and the next run retries it.
The integration status is error while any failure is pending, retrying or dead, and success when the queue is clear.
What a user sees
| Tab | Content |
|---|---|
| Overview | Protocol, active or inactive, last sync time, authentication type, the latest result per entity (records processed, failed, first error) with a retry button per entity, and the list of synced entities and directions |
| Connection | Host, company, API version, timeout, retry policy, and whether credentials and an API key exist |
| Endpoints & Payloads | The Epicor endpoints and request payloads the sync uses, with the company and version filled in |
| Field Mapping | The default mapping for each entity |
| Sync History | The sync log: one row per entity run, with start and end, direction, counts of processed, succeeded and failed records, and the errors |
| Failed Records | The dead-letter list with entity, record key, status, attempts and error. Replay one record, retry all due failures, or discard a failure |
The sync log and the failure list are the places to look when records are missing.
Limits
- Epicor is polled. The engine does not receive events from Epicor, and the shortest interval is the 15-minute tick.
- Four record types are covered. Other Epicor objects are not read or written.
- The engine has no delete handling. It creates and updates records.
- There is no field-level conflict resolution. For a bidirectional entity, each run reads from Epicor and then pushes changed OpexMX rows.
- Field mapping is by field name. Computed values and unit conversions beyond the status rules above are not part of the default mapping.
- Inbound paging sends
$topand$skipand does not add an$orderby. The OpexFlow Epicor study (see Evidence) found that unordered paging against a real Epicor test server can skip rows. Where this matters, confirm paging behaviour on the target Epicor system before relying on a full inbound read.
Where OpexFlow fits
The native sync moves master and transactional records on a schedule. It is not built to prove that one specific posting arrived, for example a goods issue against a work order. For that, OpexMX uses its outbound webhooks and OpexFlow.
What the OpexMX webhook sender does
- A webhook subscribes to event types per tenant, with wildcards such as
ticket.*. Event types includework_order.completed,part.low_stock,purchase_order.received,asset.status_changedand others. - Each delivery is a
POSTwith a JSON body. The body has a unique event id, the event type, the tenant and the record data. - Each request is signed. The
X-Webhook-Signatureheader holds a timestamp and an HMAC-SHA256 of the timestamp and body, made with the webhook secret.X-Webhook-Id,X-Webhook-TimestampandX-Webhook-Eventare also sent. The secret can be rotated. - The timeout is 30 seconds. Every attempt is written to a delivery log with status, duration and the response.
- Webhook health is calculated from the last 10 deliveries: healthy at 80% success or more, degraded at 50% or more, otherwise failing.
- After a network error or timeout the sender retries, by default up to 3 attempts with 60 seconds, then 120 seconds. A response with a non-success status is logged as a failed delivery and is not sent again by the sender.
That last point is why a receiver that keeps its own record of what it accepted and what the target confirmed is useful.
What OpexFlow adds
OpexFlow sits between the OpexMX webhook and the ERP. For postings back to an ERP it provides:
- Signature check. A request that fails it is refused and never enters the flow.
- Once per event. A repeated event id is recognised and dropped, so a resend does not post twice.
- Confirmed delivery. The flow records the event on the OpexMX side and the ERP's answer on the ERP side, and returns the ERP document reference to the sender.
- Retry. A temporary ERP failure is answered back to the sender as a retryable error rather than lost.
- Dead letters with replay. A permanent refusal from the ERP is parked with its original record. A person can correct it and replay it, and the action is recorded under their sign-in.
- Alerts. A refusal can also send an email to an operations mailbox.
- Nightly reconcile. A scheduled comparison of the two sides by event id turns any gap into an exception case with a status and an assignee.
- Health view. Calls, errors, retries and latency for each external call.
No recorded test pairs OpexMX postings with a live Epicor system. The posting pattern itself has been tested against SAP, and the Epicor read side has been tested in OpexFlow against recorded Epicor responses. The Epicor interface used for postings (REST business objects, BAQ reads) is set per installation.
Evidence
Both studies are OpexFlow case studies. Each has a scope section that states what was real and what was simulated.
- Posting pattern, tested against SAP. OpexMX to SAP: confirmed delivery for every posting. Signed webhook, once-per-event, dead letters with replay, nightly reconcile. Run against a simulated SAP middleware, not a live SAP system.
- Epicor read side, tested in OpexFlow. Epicor general ledger to Postgres: one request per partition, checked by count. The flow, load, retries, dead letters and reconcile were real local runs. The Epicor side was a mock replaying responses recorded from EPIC06, Epicor's demo company. No live Epicor or customer system was called.
That study also reports that unordered $top/$skip paging against a real Epicor test server dropped rows (12 of 17,160 on one trial balance), found in earlier probes that the mock cannot reproduce. See the limits above for how this applies to the native sync.