> For the complete documentation index, see [llms.txt](https://docs.roboflow.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.roboflow.com/deployment/self-hosted/enterprise/deployment-manager/services/event-store/rest-api.md).

# Event Store REST API

The Event Store runs on a managed device and stores inference events produced by pipelines on that device. Base URL is `http://<device-ip>:8001`, and interactive Swagger documentation is served at `/docs`. See [Services](/deployment/self-hosted/enterprise/deployment-manager/services.md#using-the-apis) for the base URL, authentication, and error-shape rules shared by all on-device service APIs.

The v2 API accepts camelCase input (ex: `base64Image`, `objectDetections`) and returns snake\_case responses.

{% hint style="warning" %}
This is not the cloud [Vision Events API](/deployment/monitoring-and-analytics/vision-events.md). The local contract uses `event_schema`, `event_data`, and `inference_timestamp` where the cloud uses `eventType`, `useCaseId`, and `timestamp`, and it takes image bytes directly rather than references to uploaded images. A cloud payload will not validate here.
{% endhint %}

If `API_KEY` is set on the service, every endpoint except `/health` requires an `X-API-Key` header. See [Authentication](/deployment/self-hosted/enterprise/deployment-manager/services/event-store.md#authentication).

```bash
curl -H "X-API-Key: $EVENT_STORE_API_KEY" \
  "http://<device-ip>:8001/v2/events/latest/query?limit=5"
```

## Create an Event

Bounding box coordinates are center-based absolute pixels: `x` and `y` are the center of the box, `width` and `height` are the full dimensions. Confidence runs from 0.0 to 1.0.

Set `draft: true` to keep the event open so a video or local-only file can be attached after the fact, then finalize it. Omitting `draft` finalizes the event on creation, so existing producers are unaffected.

{% hint style="warning" %}
`solution` is optional in the schema but conditionally required at runtime. When cloud upload is enabled on the service and `DEFAULT_SOLUTION_ID` is not set, creating an event without `solution` returns `400`. Either send it on every event or set the environment variable.
{% endhint %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/v2/events" method="post" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

## Query Events

`GET /v2/events` filters by time window, source, and custom metadata. `metadata_filter` takes `key:value` or `key:op:value` with the operators `eq`, `ne`, `gt`, `lt`, `gte`, and `lte`, and repeats with AND logic.

Per-image `metadata` is not queryable through `metadata_filter`. Only `custom_metadata` on the event is.

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/v2/events" method="get" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/v2/events/{event\_id}" method="get" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/v2/events/latest/query" method="get" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/events/count/stats" method="get" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

## Draft Lifecycle

A producer that needs to attach a video encoded after the pass/fail decision creates the event as a draft, uploads the file when it is ready, then finalizes:

```bash
EVENT_ID=$(curl -s -X POST http://<device-ip>:8001/v2/events \
  -H "Content-Type: application/json" \
  -d '{"inference_timestamp":"2025-01-30T14:30:00Z","event_schema":"quality_check","event_data":{"result":"pass"},"solution":"a1b2c3d4e5f67890","draft":true}' \
  | jq -r '.id')

curl -X POST "http://<device-ip>:8001/v2/events/$EVENT_ID/videos" -F "file=@clip.mp4"
curl -X POST "http://<device-ip>:8001/v2/events/$EVENT_ID/finalize"
```

Until an event is finalized it is skipped by cloud upload. It is also protected from cleanup, but only while cloud upload is enabled on the service. With cloud upload off, a draft is as deletable as any other record, so retention and capacity cleanup can remove one that is still being assembled.

A draft whose producer never calls finalize is force-closed after `DRAFT_AUTO_FINALIZE_SECONDS`. Both `finalized_at` and `auto_finalized_at` are set in that case, so read `auto_finalized_at` to tell a force-closed event from one the producer finalized itself.

Attachment routes add bytes, so unlike finalize they are subject to capacity backpressure: when the store is over its limits they return `529` and the upload is not accepted.

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/v2/events/{event\_id}/videos" method="post" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/v2/events/{event\_id}/local-only-files" method="post" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/v2/events/{event\_id}/finalize" method="post" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

## Event Schemas

`event_schema` selects the structure of `event_data`. Each schema forbids fields it does not define, and every field accepts both its camelCase and snake\_case spelling.

The two fields fail differently. A bad `event_schema` returns `422`. An `event_data` payload that does not conform to the named schema returns `400`, with a string `detail` naming the schema.

{% tabs %}
{% tab title="quality\_check" %}

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Constraint</th></tr></thead><tbody><tr><td><code>result</code></td><td><code>"pass"</code> or <code>"fail"</code></td><td>Yes</td><td></td></tr><tr><td><code>externalId</code></td><td>string</td><td>No</td><td>Up to 1000 characters</td></tr></tbody></table>

```json
{ "result": "pass", "externalId": "SKU-12345" }
```

{% endtab %}

{% tab title="inventory\_count" %}

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Constraint</th></tr></thead><tbody><tr><td><code>location</code></td><td>string</td><td>No</td><td>Up to 1000 characters</td></tr><tr><td><code>itemCount</code></td><td>integer</td><td>No</td><td>0 or greater</td></tr><tr><td><code>itemType</code></td><td>string</td><td>No</td><td>Up to 1000 characters</td></tr><tr><td><code>externalId</code></td><td>string</td><td>No</td><td>Up to 1000 characters</td></tr></tbody></table>

```json
{ "location": "warehouse-A", "itemCount": 42, "itemType": "widgets" }
```

{% endtab %}

{% tab title="safety\_alert" %}

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Constraint</th></tr></thead><tbody><tr><td><code>alertType</code></td><td>string</td><td>No</td><td>Up to 256 characters, letters, numbers, underscores, spaces, and hyphens only</td></tr><tr><td><code>severity</code></td><td><code>"low"</code>, <code>"medium"</code>, or <code>"high"</code></td><td>No</td><td></td></tr><tr><td><code>description</code></td><td>string</td><td>No</td><td>Up to 10000 characters</td></tr><tr><td><code>externalId</code></td><td>string</td><td>No</td><td>Up to 1000 characters</td></tr></tbody></table>

```json
{ "alertType": "no_ppe", "severity": "high", "description": "Worker entered area without hard hat" }
```

{% endtab %}

{% tab title="operator\_feedback" %}

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Constraint</th></tr></thead><tbody><tr><td><code>relatedEventId</code></td><td>string</td><td>Yes</td><td>ID of the event being rated</td></tr><tr><td><code>feedback</code></td><td><code>"correct"</code>, <code>"incorrect"</code>, or <code>"inconclusive"</code></td><td>Yes</td><td></td></tr></tbody></table>

```json
{ "relatedEventId": "550e8400-e29b-41d4-a716-446655440000", "feedback": "correct" }
```

Query these back with the `related_event_id` filter on `GET /v2/events`.
{% endtab %}

{% tab title="custom" %}

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Constraint</th></tr></thead><tbody><tr><td><code>externalId</code></td><td>string</td><td>No</td><td>Up to 1000 characters</td></tr><tr><td><code>value</code></td><td>string</td><td>No</td><td>Up to 10000 characters</td></tr></tbody></table>

```json
{ "externalId": "sensor-123", "value": "temperature:72.5" }
```

For structured data that does not fit these fields, use `custom_metadata` on the event, which is queryable, or per-image `metadata`, which is not.
{% endtab %}
{% endtabs %}

## Download Files

Image IDs are ephemeral. Cleanup can remove a file at any time, so handle `404` and do not cache IDs beyond a single session. Compare `current_file_count` against `original_file_count` on an event to tell whether its files were cleaned up.

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/images/{image\_id}" method="get" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/videos/{video\_id}" method="get" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/local-only-files/{file\_id}" method="get" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

## Statistics and Health

`/stats` reports current usage, the active configuration, capacity against each limit, and what the next cleanup pass will delete. Alert on `capacity.storage.percent_used` and `capacity.records.percent_used` above 80%.

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/stats" method="get" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/health" method="get" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

## Administration

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/admin/cleanup" method="post" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

Resetting takes two calls so it cannot happen by accident. `/reset/request` returns a single-use token valid for 60 seconds, and `/reset/confirm` performs the deletion.

{% hint style="danger" %}
A confirmed reset permanently deletes every event and file in the store. It cannot be undone.
{% endhint %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/reset/request" method="post" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/I83YpxmNh4upE1YTiRTx" path="/reset/confirm" method="post" %}
[edge-event-store.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-649d4e0cb1dff61d7b5026b17f0f0793798ea350%2Fedge-event-store.yaml?alt=media)
{% endopenapi %}
