> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnilinker.pl/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent local API reference

> The HTTP API the ERP Sync agent serves on the machine it runs on: its routes, parameters and responses.

The ERP Sync agent runs a small HTTP API on the computer where it is installed. You can use it to check the agent's
status, read its recent log entries, and pause, resume or trigger syncing from a script or a monitoring tool.

This is the agent's **local** API. It is not Omnilinker's cloud API and it is not reachable from the internet. For
calling Omnilinker itself, see the [Developers](/developers/overview) section.

## Base URL

```text theme={null}
http://localhost:5555
```

The port comes from the `LocalApiPort` setting in the agent's `appsettings.json` (default `5555`). Leave it at the
default: the tray app's fallback connection and the web app's **Local Service** tab always use port 5555. See the
[configuration reference](/erp/configuration-reference).

The API uses plain HTTP. It is served by the Windows service, so it is available only while the service is running.

## Access and security

* **Localhost only.** The agent listens on the loopback addresses only (`127.0.0.1` and `::1`). Other computers on
  your network cannot connect to it, and it needs no firewall rule. A request that names any host other than
  `localhost` or a loopback address is refused with `403 Forbidden`.
* **One required header.** Every request to a route under `/api` must carry the header `X-Omnilinker-Local: 1`.
  Without it, the agent answers `403 Forbidden` with an empty body. `/health` does not check the header; sending it
  there does no harm.
* **Browsers: Omnilinker only.** A web page cannot add that header without asking the agent first (a CORS
  preflight). The agent agrees only for Omnilinker's own address, `https://omnilinker.pl`, and its subdomains, over
  HTTPS. That is how the web app's **Local Service** tab, open in a browser on the agent computer, reads the API.
  Requests from any other website are refused with `403`.
* **No authentication.** Programs on the computer, such as the tray app, PowerShell or `curl`, send the header and
  need no API key, password or token.

```powershell theme={null}
Invoke-RestMethod -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/status
```

<Warning>
  **Anyone who can run a program on the agent computer can call the API.** They can read the agent's status and log
  entries, and pause, resume or trigger syncing. The API does not check the **Omnilinker ERP Sync Operators** group
  that the tray app uses. It never returns the API key, the ERP password or the connection string, and it cannot
  change the agent's settings. Still, treat access to the agent computer as access to the agent: limit who can sign
  in to it.
</Warning>

## Conventions

* Request and response bodies are JSON. Property names are camelCase.
* Times are in UTC, in ISO 8601 format, for example `2026-09-27T08:15:02.1234567Z`.
* Enumerations are returned as **numbers**, not names. The tables on this page list the values.
* Control routes return `400 Bad Request` with a [command response](#command-response) when the agent refuses the
  command.
* `403 Forbidden` with an empty body means the request was refused before it reached the agent: the
  `X-Omnilinker-Local` header is missing, the host is not `localhost`, or a browser sent it from another website.
* The examples use PowerShell. In Windows PowerShell 5.1, `curl` is an alias for `Invoke-WebRequest`. Type
  `curl.exe` to use curl.

## Routes

| Method | Route                    | Purpose                                                 |
| ------ | ------------------------ | ------------------------------------------------------- |
| `GET`  | `/health`                | Health of the agent and its connections, as plain text. |
| `GET`  | `/api/sync/health`       | Liveness check: is the API answering.                   |
| `GET`  | `/api/sync/status`       | Full status of the agent.                               |
| `GET`  | `/api/sync/config`       | Agent version, state and whether it is paused.          |
| `POST` | `/api/sync/now`          | Ask the agent to look for changes now.                  |
| `POST` | `/api/sync/trigger`      | Same as `/api/sync/now`.                                |
| `POST` | `/api/sync/pause`        | Pause syncing.                                          |
| `POST` | `/api/sync/resume`       | Resume syncing.                                         |
| `GET`  | `/api/sync/logs`         | Recent entries of the agent's sync log.                 |
| `GET`  | `/api/sync/outbox/stats` | Counts of events in the agent's outbox.                 |
| `GET`  | `/api/sync/hash/status`  | State of hash-based change detection.                   |
| `GET`  | `/api/sync/hash/stats`   | Size of the agent's hash store.                         |
| `POST` | `/api/sync/hash/trigger` | Start a hash check now.                                 |

No other routes exist.

## Health

### GET /health

Runs the agent's health checks and returns the overall result as plain text: `Healthy`, `Degraded` or `Unhealthy`.
The HTTP status is `200 OK` for `Healthy` and `Degraded`, and `503 Service Unavailable` for `Unhealthy`.

| Check            | Healthy                                     | Degraded                                | Unhealthy                           |
| ---------------- | ------------------------------------------- | --------------------------------------- | ----------------------------------- |
| Local database   | The agent's local database answers.         |                                         | It does not.                        |
| ERP connection   | The agent is connected to the ERP database. |                                         | It is not.                          |
| SQL connection   | The agent is connected to the ERP database. | The ERP database is not configured yet. | It is configured but not connected. |
| Cloud connection | Omnilinker (`omnilinker.pl`) answers.       | Omnilinker answers with an error.       | Omnilinker cannot be reached.       |

`/health` returns `Unhealthy` until the agent has connected to the ERP database, for example right after the service
starts or before the [setup wizard](/erp/setup-wizard) has run. Use `/api/sync/health` to
check only that the service is running.

```powershell theme={null}
Invoke-WebRequest -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/health -UseBasicParsing |
  Select-Object StatusCode, Content
```

### GET /api/sync/health

Returns `200 OK` whenever the API is answering. It does not check the ERP or Omnilinker.

<ResponseField name="status" type="string">
  Always `healthy`.
</ResponseField>

<ResponseField name="timestamp" type="string">
  The agent's current time, UTC.
</ResponseField>

```bash theme={null}
curl -H "X-Omnilinker-Local: 1" http://localhost:5555/api/sync/health
```

```json theme={null}
{ "status": "healthy", "timestamp": "2026-09-27T08:15:02.1234567Z" }
```

## Status

### GET /api/sync/status

Returns the agent's full status. The tray app's status window and the web app's **Local Service** tab show the same
data.

<ResponseField name="state" type="integer">
  The agent's state.

  | Value | State    | Meaning                                                                          |
  | ----- | -------- | -------------------------------------------------------------------------------- |
  | `0`   | Idle     | Ready, waiting for the next cycle.                                               |
  | `1`   | Syncing  | A sync is running, or a `POST /api/sync/now` request is waiting to be picked up. |
  | `2`   | Paused   | Syncing is paused.                                                               |
  | `3`   | Error    | The last cycle failed. See `lastError`.                                          |
  | `4`   | Starting | The service is starting and has not loaded its configuration yet.                |
  | `5`   | Stopping | The service is shutting down.                                                    |
</ResponseField>

<ResponseField name="isConnected" type="boolean">
  Whether the agent's last request to Omnilinker succeeded.
</ResponseField>

<ResponseField name="isErpConnected" type="boolean">
  Whether the agent is connected to the ERP database.
</ResponseField>

<ResponseField name="lastSyncTime" type="string | null">
  When the last sync cycle finished successfully.
</ResponseField>

<ResponseField name="nextSyncTime" type="string | null">
  When the next cycle is expected, if known.
</ResponseField>

<ResponseField name="errorCount" type="integer">
  Errors since the last successful cycle.
</ResponseField>

<ResponseField name="lastError" type="string | null">
  The last error message, for example `Waiting for cloud configuration`.
</ResponseField>

<ResponseField name="waitingReason" type="string | null">
  Why a cycle is delayed, for example `Product polling delayed (HashCheck in progress)`.
</ResponseField>

<ResponseField name="suggestedActions" type="integer[]">
  Fixes the tray app offers for the last error. `1` test ERP connection, `2` test cloud connection, `3` check
  settings, `4` retry now, `5` view logs, `6` check network, `7` contact support, `8` view details, `9` skip item.
</ResponseField>

<ResponseField name="outbox" type="object">
  Events in the agent's outbox, with `pending`, `sent`, `failed` and `retrying`. The same numbers as
  `/api/sync/outbox/stats`.
</ResponseField>

<ResponseField name="entityCounts" type="object">
  Outbox counts per entity type, keyed by type name (for example `Product`, `Brand`, `PriceLevel`, `Price`,
  `Warehouse`, `Stock`). Each value has `total`, `synced`, `pending` and `failed`. Only events still in the local
  outbox are counted: sent events are removed after 24 hours (1 hour during the initial sync).
</ResponseField>

<ResponseField name="version" type="string">
  The agent's version.
</ResponseField>

<ResponseField name="updateAvailable" type="boolean">
  Not used: always `false`. The agent [updates itself](/erp/install-agent#update-the-agent), but does not report a
  waiting update here.
</ResponseField>

<ResponseField name="updateVersion" type="string | null">
  Not used: always `null`.
</ResponseField>

<ResponseField name="hashCheck" type="object | null">
  The same object as `/api/sync/hash/status`.
</ResponseField>

<ResponseField name="currentHashCheckProgress" type="object | null">
  While a hash check runs: `productsChecked`, `totalProducts`, `brandsChecked`, `totalBrands`, `elapsed`,
  `estimatedTimeRemaining` and `itemsPerSecond`.
</ResponseField>

<ResponseField name="queueDepth" type="object | null">
  `currentPending`, `healthLevel` (`0` normal, fewer than 50 pending; `1` elevated, 50 to 199; `2` backlogged, 200
  or more), `trend` (`0` steady, `1` growing, `2` draining), `trendItemsPerMinute` and `trendDescription`.
</ResponseField>

<ResponseField name="currentOperations" type="object[]">
  Operations running now, such as polling or publishing, with their item counts and progress.
</ResponseField>

<ResponseField name="recentActivity" type="object[]">
  The most recently completed operations, with `completedAt`, `itemCount`, `duration`, `success` and
  `errorMessage`.
</ResponseField>

<ResponseField name="systemHealth" type="object">
  Health of four components: `erpConnection`, `cloudApi`, `database` and `credentials`. Each has a `level` (`0`
  healthy, `1` warning, `2` critical), a `message` and the time of the last check. `overallHealth` is the worst of the
  four, and `issueCount` counts those that are not healthy.
</ResponseField>

The objects under `currentOperations`, `recentActivity` and `systemHealth` also carry display fields used by the
tray app. Do not rely on their exact shape; it can change between agent versions.

```powershell theme={null}
$status = Invoke-RestMethod -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/status
$status | Select-Object state, isConnected, isErpConnected, lastSyncTime, lastError
$status.outbox
```

A shortened response:

```json theme={null}
{
  "state": 0,
  "isConnected": true,
  "isErpConnected": true,
  "lastSyncTime": "2026-09-27T08:15:02.1234567Z",
  "nextSyncTime": null,
  "errorCount": 0,
  "lastError": null,
  "waitingReason": null,
  "suggestedActions": [],
  "outbox": { "pending": 0, "sent": 245, "failed": 0, "retrying": 0 },
  "entityCounts": {
    "Product": { "total": 240, "synced": 240, "failed": 0, "pending": 0 },
    "Brand": { "total": 5, "synced": 5, "failed": 0, "pending": 0 }
  },
  "version": "<agent version>",
  "updateAvailable": false,
  "updateVersion": null
}
```

### GET /api/sync/config

Returns a few facts about the agent. It does not return its settings or credentials.

<ResponseField name="version" type="string">
  The agent's version.
</ResponseField>

<ResponseField name="isPaused" type="boolean">
  Whether syncing is paused.
</ResponseField>

<ResponseField name="state" type="string">
  The agent's state as a **name**, for example `Idle` (unlike `/api/sync/status`, which returns a number).
</ResponseField>

```powershell theme={null}
Invoke-RestMethod -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/config
```

## Control

These routes take no parameters and no request body.

### Command response

Every control route, and `POST /api/sync/hash/trigger`, returns this object:

<ResponseField name="success" type="boolean">
  Whether the agent accepted the command.
</ResponseField>

<ResponseField name="message" type="string">
  What happened, or why the command was refused.
</ResponseField>

### POST /api/sync/now

Asks the agent to look for changes now. `POST /api/sync/trigger` does exactly the same.

The request is picked up at the agent's next polling cycle, within `PollingIntervalSeconds` (60 seconds by default).
With hash scan change detection it starts a hash check.

| HTTP status | `message`                       | When                                               |
| ----------- | ------------------------------- | -------------------------------------------------- |
| `200`       | `Sync started`                  | Accepted.                                          |
| `400`       | `Sync is paused. Resume first.` | Syncing is paused.                                 |
| `400`       | `Sync is already in progress.`  | A sync is running or a request is already waiting. |

```powershell theme={null}
Invoke-RestMethod -Method Post -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/now
```

```bash theme={null}
curl -X POST -H "X-Omnilinker-Local: 1" http://localhost:5555/api/sync/now
```

### POST /api/sync/pause

Pauses syncing. The agent stops detecting new changes until you resume. Always returns `200` with `Sync paused`.

* Events already in the outbox are still sent to Omnilinker while the agent is paused.
* A pause is not kept when the service restarts.

```powershell theme={null}
Invoke-RestMethod -Method Post -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/pause
```

### POST /api/sync/resume

Resumes syncing. Always returns `200` with `Sync resumed`.

```powershell theme={null}
Invoke-RestMethod -Method Post -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/resume
```

## Logs

### GET /api/sync/logs

Returns the newest entries of the agent's own sync log, newest first. These are the entries the tray app's log
viewer shows. They are not the Omnilinker sync log entries described in [Monitoring](/erp/monitoring#sync-logs), and
not the service's log files.

<ParamField query="count" type="integer" default="50">
  How many entries to return. At most 500; larger values return 500.
</ParamField>

<ParamField query="level" type="string">
  Minimum level: `Debug`, `Info`, `Warning`, `Error` or `Critical` (or `0` to `4`). Returns entries at this level and
  above.
</ParamField>

<ParamField query="category" type="string">
  Only entries of this category, for example `ErpPolling`, `HashCheck` or `ChangeConsumption`. Exact match.
</ParamField>

<ParamField query="entityType" type="string">
  Only entries for this entity type. Exact match.
</ParamField>

The response is an array of log entries:

<ResponseField name="id" type="integer">
  The entry's ID.
</ResponseField>

<ResponseField name="timestamp" type="string">
  When the entry was written.
</ResponseField>

<ResponseField name="level" type="integer">
  `0` Debug, `1` Info, `2` Warning, `3` Error, `4` Critical.
</ResponseField>

<ResponseField name="message" type="string">
  The message.
</ResponseField>

<ResponseField name="details" type="string | null">
  Extra details, if any.
</ResponseField>

<ResponseField name="category" type="string | null">
  The component that wrote the entry.
</ResponseField>

<ResponseField name="entityType" type="string | null">
  The entity type concerned, if any.
</ResponseField>

<ResponseField name="entityId" type="string | null">
  The entity concerned, if any.
</ResponseField>

<ResponseField name="exception" type="string | null">
  The exception, for errors.
</ResponseField>

```powershell theme={null}
Invoke-RestMethod -Headers @{ "X-Omnilinker-Local" = "1" } "http://localhost:5555/api/sync/logs?count=20&level=Warning" |
  Format-Table timestamp, level, category, message
```

```bash theme={null}
curl -H "X-Omnilinker-Local: 1" "http://localhost:5555/api/sync/logs?count=20&level=Warning"
```

## Outbox

### GET /api/sync/outbox/stats

The agent queues every change in a local outbox and sends it to Omnilinker in batches. This route counts the events
in the outbox by state.

<ResponseField name="pending" type="integer">
  Waiting to be sent, including events that failed an attempt and will be tried again.
</ResponseField>

<ResponseField name="sent" type="integer">
  Sent successfully and not yet cleaned up. Sent events are removed after 24 hours (1 hour during the initial sync).
</ResponseField>

<ResponseField name="failed" type="integer">
  Failed on every attempt (`MaxRetryAttempts`, 5 by default). The agent does not send these again.
</ResponseField>

<ResponseField name="retrying" type="integer">
  Being sent right now. Despite the name, this is not a count of events waiting for a retry: those are in `pending`.
</ResponseField>

```powershell theme={null}
Invoke-RestMethod -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/outbox/stats
```

```json theme={null}
{ "pending": 5, "sent": 1245, "failed": 0, "retrying": 0 }
```

A `pending` count that keeps growing means the agent cannot send to Omnilinker. See
[Troubleshooting](/erp/troubleshooting).

## Hash check

A hash check compares the ERP data with what the agent last sent, to find changes. It is how the agent detects
changes with hash scan change detection. See [Wapro](/erp/providers/wapro).

### GET /api/sync/hash/status

<ResponseField name="isEnabled" type="boolean">
  Whether hash checks are enabled in the connection's sync configuration. With WFM\_INT change tracking, a weekly
  reconciliation check always runs and this is `true`.
</ResponseField>

<ResponseField name="isRunning" type="boolean">
  Whether a hash check is running now.
</ResponseField>

<ResponseField name="isPaused" type="boolean">
  Whether hash checks are paused on their own (from the tray app).
</ResponseField>

<ResponseField name="lastCheckTime" type="string | null">
  When the last hash check finished.
</ResponseField>

<ResponseField name="nextCheckTime" type="string | null">
  When the next one is due.
</ResponseField>

<ResponseField name="lastProductsChecked" type="integer">
  Products compared in the last check. `lastProductsChanged`, `lastBrandsChecked` and `lastBrandsChanged` work the
  same way.
</ResponseField>

<ResponseField name="totalStoredProductHashes" type="integer">
  Products in the hash store. `totalStoredBrandHashes` is the same for brands.
</ResponseField>

<ResponseField name="lastError" type="string | null">
  The last hash check error.
</ResponseField>

```powershell theme={null}
Invoke-RestMethod -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/hash/status
```

### GET /api/sync/hash/stats

<ResponseField name="totalProducts" type="integer">
  Products in the hash store.
</ResponseField>

<ResponseField name="totalBrands" type="integer">
  Brands in the hash store.
</ResponseField>

<ResponseField name="lastHashCheck" type="string | null">
  When the store was last updated by a hash check.
</ResponseField>

```powershell theme={null}
Invoke-RestMethod -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/hash/stats
```

### POST /api/sync/hash/trigger

Starts a hash check now, without waiting for its interval.

| HTTP status | `message`                                                                               | When                                                                                        |
| ----------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `200`       | `Hash check started`                                                                    | Accepted.                                                                                   |
| `400`       | `Hash check is disabled in cloud configuration. Enable it in the web dashboard.`        | Hash checks are disabled in the connection's [sync configuration](/erp/sync-configuration). |
| `400`       | `Hash check is already running. Wait for current check to complete or cancel it first.` | A check is running.                                                                         |

```powershell theme={null}
Invoke-RestMethod -Method Post -Headers @{ "X-Omnilinker-Local" = "1" } http://localhost:5555/api/sync/hash/trigger
```

## Named pipe

The tray app talks to the service mainly over a Windows named pipe, `OmnilinkerErpSync`, and falls back to this HTTP
API when the service does not answer on the pipe. The pipe is an internal interface between the two parts of the
agent, not a public API: its commands can change between agent versions. Use the HTTP routes on this page for
scripts and monitoring.

* **Who can connect:** SYSTEM, administrators, and people signed in to the computer, at the console or over Remote
  Desktop.
* **What they can do:** anyone connected can read the status, logs and settings. Commands that change the agent,
  its ERP database or its credentials, or that pause it, need a member of **Omnilinker ERP Sync Operators** or an
  administrator whose tray app runs elevated. Others get the error "Only an administrator of this PC can do that."
  (code `Pipe:NotAllowed`). See [Who can change the agent](/erp/install-agent#who-can-change-the-agent).
* **Which service:** the tray app talks only to the Windows service. It does not trust another program that opens
  a pipe with the same name.
