# Calling the Kinetic REST v2 API

> Kinetic REST v2 in practice, covering the URL shape, API keys, how $filter and $expand behave, why child collections come back empty and paging.

Source: https://docs.lumina-erp.com/epicor-kinetic/rest-v2-api/

**In short.** REST v2 URLs name the company and the service, every call carries an API key, child collections need $expand and large reads must page. Most integration surprises come from those four facts.

Kinetic exposes its business objects, BAQs and Epicor Functions over REST. We recommend version 2 of that API for new work. It follows OData conventions closely, which is convenient once you know them and confusing when an OData behavior looks like a bug. Below is what we explain to every developer on their first Kinetic integration.

:::note[Your instance is the reference]
Details such as the exact host, instance name, authentication method and enabled features depend on how your Kinetic environment is set up. Your instance has built-in interactive API help and a machine-readable service description. Treat those as the reference for your version.
:::

## The URL shape

Business object endpoints follow this pattern:

```text
https://{server}/{instance}/api/v2/odata/{Company}/Erp.BO.{Service}Svc/{EntitySet}
```

| Segment | Meaning |
|---|---|
| `{server}/{instance}` | Your Kinetic application server and instance |
| `api/v2/odata` | Selects REST version 2 |
| `{Company}` | The company ID. It is part of every v2 business object URL, so a call is always scoped to a company |
| `Erp.BO.{Service}Svc` | The business object service, for example `Erp.BO.SalesOrderSvc` or `Erp.BO.CustomerSvc` |
| `{EntitySet}` | The collection inside the service, for example `SalesOrders` or `Customers` |

So a read of sales orders for company `ACME` looks like:

```http
GET https://erp.example.com/KineticLive/api/v2/odata/ACME/Erp.BO.SalesOrderSvc/SalesOrders?$top=10
```

Business object **methods** are called by name with a POST to the service, with the method's parameters in a JSON body:

```http
POST https://erp.example.com/KineticLive/api/v2/odata/ACME/Erp.BO.SalesOrderSvc/{MethodName}
```

Appending `$metadata` to a service URL returns the OData description of that service: its entity sets, fields, keys and navigation properties. When in doubt about a name, read the metadata rather than guessing.

## Authentication and API keys

A v2 call carries two kinds of credentials, shown in the table.

| Credential | Sent as | What it decides |
|---|---|---|
| User authentication | Basic authentication with a Kinetic user, or a bearer token from an identity provider, depending on configuration | Whose permissions the call runs with |
| API key | The `x-api-key` header | Which calling application this is. Keys are created in Kinetic and can be tied to an access scope that limits which services, BAQs and functions the key can reach |

A minimal request with curl:

```bash
curl -s \
  -H "Authorization: Bearer $KINETIC_TOKEN" \
  -H "x-api-key: $KINETIC_API_KEY" \
  -H "Accept: application/json" \
  "https://erp.example.com/KineticLive/api/v2/odata/ACME/Erp.BO.CustomerSvc/Customers?\$select=CustID,Name&\$top=5"
```

Adopt these practices early:

- Use one API key per integration, each with the narrowest access scope that works. A leaked key then exposes one integration, not the whole system.
- Run calls as a dedicated integration user rather than a personal account, so a departure or password change does not break production.
- Keep secrets in a secrets manager, never in source code or in a shared script.
- Plan rotation. Know how you will replace a key without downtime before you need to.

## Reading data with $filter, $select and $orderby

The standard OData query options work as you would expect:

| Option | Purpose | Example |
|---|---|---|
| `$filter` | Restrict rows | `$filter=OrderDate ge 2026-01-01T00:00:00Z` |
| `$select` | Return only named fields | `$select=OrderNum,CustNum,OrderDate` |
| `$orderby` | Sort | `$orderby=OrderNum desc` |
| `$top` | Limit rows | `$top=100` |
| `$skip` | Skip rows, for paging | `$skip=200` |
| `$expand` | Include related child records | `$expand=OrderDtls` |

:::tip[Always use $select]
Business object entities are wide, and returning every field for thousands of rows is slow for the server and for your integration.
:::

String values in `$filter` are quoted with single quotes, and a single quote inside a value is escaped by doubling it. URL-encode the whole query string.

## Empty child collections and $expand

This is the behavior that surprises almost everyone.

A business object entity has **navigation properties** for its child records: a sales order has lines, a line has releases. As with other OData services, child collections are not populated unless you ask for them with `$expand`.

They can come back as **empty** collections when you query the top-level entity set, for example with a `$filter`. The order comes back, and it looks as if it has no lines. Nothing is wrong with the order. OData does not include related records unless you request them:

```http
GET .../Erp.BO.SalesOrderSvc/SalesOrders?$filter=OrderNum eq 5001&$expand=OrderDtls&$select=OrderNum,CustNum
```

A few more points apply to expansion:

- Use the navigation property name from `$metadata`. It is similar to the table name you know from BAQs, but not always identical.
- You can shape the expansion. OData allows options inside an expand, such as selecting fields of the child: `$expand=OrderDtls($select=OrderLine,PartNum,OrderQty)`.
- Expansion multiplies payload size. Expanding lines and releases for a thousand orders can produce a large response. Page the parent and select narrowly.
- If you only need child rows, query them directly. Many services expose child entity sets, or a BAQ can return the joined rows you need at the right grain.

## Paging through large result sets

Large result sets must be paged.

:::caution[The server applies row limits]
A request that asks for more rows than the configured limit, or does not specify a limit, may return fewer rows than exist. Do not assume one call returned everything.
:::

`$top` and `$skip` are the paging tools:

1. Request a page with `$top`, with an `$orderby` on a unique key.
2. Advance with `$skip`.
3. Stop when a page returns fewer rows than you asked for.

Always include the `$orderby` on a unique key. Without an explicit order, rows can shift between pages as data changes, and you can miss or repeat records. For large extracts, prefer filtering on a changed-since date or a key range over deep `$skip` values, which get slower as they grow.

## BAQs over REST

A BAQ can be run over REST, which is often the cleanest way to read joined data at the grain you want. The v2 BAQ endpoint is rooted in the same company path, under a BAQ service, and returns the BAQ rows. OData options such as `$filter`, `$select` and `$top` can be applied to the result.

This moves query design into Kinetic, where it can be tested in the designer, and keeps the integration simple. Build the BAQ with care, because everything in [BAQ fundamentals](/epicor-kinetic/baq-fundamentals/) applies.

## Epicor Functions over REST

Published Epicor Functions are callable over REST under an `efx` path that includes the company, the library and the function name, with the function's input parameters in a JSON body. This is the preferred way to expose a custom server-side operation to an external system. See [Epicor Functions, BPMs or customizations?](/epicor-kinetic/functions-bpms-customizations/).

## Writing data through business object methods

Writes go through business object methods so that the Kinetic business logic runs. The usual pattern mirrors what the screen does: get a new or existing record, change fields and call the update method, sometimes with intermediate methods that apply defaults. Tracing what the screen calls for the same action is the fastest way to find the right sequence.

For every write integration:

- Carry an external reference you can check for, so a retried call does not create a duplicate.
- Validate before calling, and log the request and response for every failed call.
- Expect business errors, such as credit holds or missing setup, to come back as errors from the method. Handle them as data problems for a person to resolve, not as retryable faults.

## Troubleshooting checklist

Start with these checks when a call misbehaves.

| Symptom | Check |
|---|---|
| 401 or 403 | Both credentials. A valid user with a missing or out-of-scope API key still fails |
| Empty child collections | Add `$expand` |
| Fewer rows than expected | Paging or server row limits |
| Field not found | The exact name in `$metadata` |
| Works in the browser help, fails in code | Compare headers, especially `x-api-key`, and the company in the URL |

## Sources

- [Open REST API (Epicor)](https://www.epicor.com/en/products/enterprise-resource-planning-erp/epicor-kinetic/tools-and-technology/open-rest-api/)
- [REST Services V2 (EpiUsers forum, answer by an Epicor employee)](https://www.epiusers.help/t/rest-services-v2/57725)
- [REST API returns only 100 results (EpiUsers forum, answer by an Epicor employee)](https://www.epiusers.help/t/rest-api-for-jobentrysvc-jobentries-only-returns-100-results/54756)

---

Epicor, Prophet 21, P21 and DynaChange are trademarks or registered trademarks of Epicor Software Corporation registered in the United States and other countries. Kinetic is a trademark of Epicor Software Corporation. Lumina ERP is an independent consultancy and is not affiliated with, sponsored by or endorsed by Epicor.
