API reference
Manage listeners, streams, destinations, metrics and alerts, read events and usage, and ingest events — everything the app does, over a small JSON API authenticated with organization API keys.
https://app.eventeer.io/api/v1
Quick start
Three commands from zero to a live event feed.
-
1
Create a listener
POST /api/v1/listeners returns its
ingress_url. -
2
Send an event
Any HTTP client can POST JSON to the
ingress_url. -
3
Watch it live
Subscribe to a stream over WebSocket (
/out/s/…).
# 1. create a webhook listener curl -X POST https://app.eventeer.io/api/v1/listeners \ -H "Authorization: Bearer $EVENTEER_KEY" -H "Content-Type: application/json" \ -d '{"name":"orders","type":"webhook","auth_mode":"anonymous"}' # 2. send an event to its ingress_url curl -X POST https://app.eventeer.io/in/w/lst_7Qp2xK9vLmN3rT5wY8zA1b -H "Content-Type: application/json" \ -d '{"type":"order.created","amount":42.5}' # 3. create a stream + token, then watch events live websocat "wss://app.eventeer.io/out/s/str_4Hc8nV2qWm6Rt1yZp0Lk3x?token=st_…"
Authentication
Every request carries an organization API key as a Bearer token.
Create keys in Settings → API keys
inside your organization (owners and admins). The key (evk_…) is shown once; it acts on the whole organization, so keep it server-side. Missing or invalid keys get 401 unauthorized; a key without the scope a route needs gets 403 forbidden.
Authorization: Bearer evk_3Fq9Lm2Xv7Rt5Nb8Kc1Wd4Hy6Zp0Js
Every GET endpoint: list and read resources, events, series and usage.
Every POST, PATCH/PUT and DELETE endpoint (create, change, delete, pause, retry, …). Grant read too if the client also reads.
Send events: POST /api/v1/listeners/:key/events, and token-mode listeners (webhook and device ingress).
/api/v1/me
The calling API key (name, prefix, scopes) and its organization.
readcurl https://app.eventeer.io/api/v1/me -H "Authorization: Bearer $EVENTEER_KEY"
{ "data": { "api_key": {"id": "…", "name": "Production backend", "prefix": "evk_3Fq9", "scopes": ["read", "write"]}, "organization": {"id": "…", "name": "Acme", "slug": "acme"} } }
Conventions
The same rules apply to every endpoint.
- JSON in, JSON out
- Send
Content-Type: application/json. Timestamps are ISO 8601 in UTC. - Identifiers
- Listeners, streams and destinations are addressed in URLs by their public
key(lst_…,str_…,dst_…); everything else by a UUID. References in bodies (listener_ids,listener_id,metric_id,device_id,destination_id) are always UUIDs (theidfield). Ids of another organization answer404. - Response shape
- A resource is
{"data": {…}}; a list is{"data": […], "next": cursor | null}. The one exception is a metric series, returned top-level as{"metric_id", "aggregation", "from", "to", "step", "points"}. - Status codes
- Create →
201, delete →204(empty body); ingest, delivery retry and device messages →202.PUTis accepted as an alias ofPATCH. - Secrets
- Credential tokens and signing secrets are returned once, at creation or rotation.
- Idempotency
- Event ingestion accepts an
Idempotency-Keyheader; repeats return200withduplicate: true.
Pagination
Lists are cursor-based and ordered by id (events and the delivery log: newest first). Pass
limit
(default 50, max 200) and, for the next page, the next
cursor of the previous response as after.
next
is null
on the last page.
curl "https://app.eventeer.io/api/v1/events?limit=2" \ -H "Authorization: Bearer $EVENTEER_KEY" # next page: pass the cursor back as `after` curl "https://app.eventeer.io/api/v1/events?limit=2&after=0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a40" \ -H "Authorization: Bearer $EVENTEER_KEY"
// events and the delivery log are newest first: `after` pages to // older entries. Every other list is in ascending id order. { "data": [ {"id": "0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a41", "listener": "lst_7Qp2xK9vLmN3rT5wY8zA1b", …}, {"id": "0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a40", "listener": "lst_7Qp2xK9vLmN3rT5wY8zA1b", …} ], "next": "0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a40" }
Rate limits
- Starter
- 20 req/s
- Growth
- 100 req/s
- Scale
- 500 req/s
Requests per second per API key, set by the plan (organizations without an active plan get Starter's). Bursts up to the same number are allowed. Beyond that the API answers 429 rate_limited with a Retry-After header (seconds) — wait and retry. Ingress has its own per-listener limit that also depends on the plan. GET /api/v1/usage returns the rates in effect for your organization (rate_limits), including any custom limit agreed with Eventeer.
Errors
Errors use a single shape: a machine-readable error
code, a human message
and, for validation errors, details
mapping each field to its messages.
| Status | Code | When |
|---|---|---|
| 400 |
invalid_json
API ingest only: the body is not storable JSON (also |
API ingest only: the body is not storable JSON (also invalid_body, invalid_idempotency_key).
|
| 401 |
unauthorized
Missing, malformed, unknown or revoked API key. |
Missing, malformed, unknown or revoked API key. |
| 402 |
plan_required
The organization has no active plan (ingest only — reads and configuration still work). |
The organization has no active plan (ingest only — reads and configuration still work). |
| 402 |
spend_cap_reached
The overage spend cap is reached (ingest only). |
The overage spend cap is reached (ingest only). |
| 403 |
forbidden
The key lacks the scope the route needs, the organization is suspended, or (API ingest) the caller's IP isn't in the listener's |
The key lacks the scope the route needs, the organization is suspended, or (API ingest) the caller's IP isn't in the listener's allowed_ips.
|
| 403 |
plan_limit
Creating the resource would exceed a hard limit of the plan (listeners, metrics, …), or re-activating a resource the plan paused. |
Creating the resource would exceed a hard limit of the plan (listeners, metrics, …), or re-activating a resource the plan paused. |
| 404 |
not_found
Unknown id/key, or it belongs to another organization. |
Unknown id/key, or it belongs to another organization. |
| 409 |
device_offline
A device message was sent to a device with no open connection. |
A device message was sent to a device with no open connection. |
| 413 |
payload_too_large
API ingest body over 256 KB, or a device message |
API ingest body over 256 KB, or a device message data over 64 KB.
|
| 415 |
unsupported_media_type
API ingest with a non-JSON |
API ingest with a non-JSON Content-Type.
|
| 422 |
validation_failed
The body or query is invalid; |
The body or query is invalid; details maps each field (or query param) to its messages.
|
| 422 |
not_retryable
Retrying a delivery that isn't |
Retrying a delivery that isn't failed or dead (destination_disabled: its destination is disabled).
|
| 422 |
not_device_listener
Registering a device on a listener that isn't a |
Registering a device on a listener that isn't a device listener.
|
| 423 |
listener_paused
API ingest into a paused listener ( |
API ingest into a paused listener (plan_paused: paused by the plan limits).
|
| 429 |
rate_limited
More requests per second with this key than the plan allows (Starter 20, Growth 100, Scale 500 — or, for API ingest, the listener's rate). Wait |
More requests per second with this key than the plan allows (Starter 20, Growth 100, Scale 500 — or, for API ingest, the listener's rate). Wait Retry-After seconds.
|
HTTP/1.1 422 Unprocessable Entity { "error": "validation_failed", "message": "The request is invalid.", "details": { "name": ["can't be blank"], "type": ["is invalid"] } }
Listeners
A listener receives events: webhook (HTTP POST), device (persistent WebSocket) or ws_source (Eventeer connects to an external feed). Listeners are addressed by their public key (lst_…); other resources reference them by id. type can't change after creation. auth_mode is anonymous (default), token or hmac. A ws_source listener's config.headers values are never returned (shown as null; send null back to keep a value). Changing config.url to another origin clears stored header values (those not re-entered in the same request), reports them in headers_cleared and emails owners/admins. plan_paused is true when a plan downgrade paused the listener.
/api/v1/listeners
List listeners (paginated, ascending by id).
read/api/v1/listeners
Create a listener: name, type, auth_mode, config, rate_limit_per_sec. The response includes ingress_url (null for ws_source).
/api/v1/listeners/:key
Show a listener.
read/api/v1/listeners/:key
Update name, auth_mode, config, rate_limit_per_sec (a different type → 422).
(or PUT)
/api/v1/listeners/:key
Delete a listener and its credentials.
write/api/v1/listeners/:key/pause
Pause: ingress answers 423.
/api/v1/listeners/:key/resume
Resume a paused listener (403 plan_limit if the plan paused it).
curl -X POST https://app.eventeer.io/api/v1/listeners \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"orders","type":"webhook","auth_mode":"token"}'
{ "data": { "id": "7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71", "key": "lst_7Qp2xK9vLmN3rT5wY8zA1b", "name": "orders", "type": "webhook", "auth_mode": "token", "status": "active", "plan_paused": false, "config": {}, "rate_limit_per_sec": null, "ingress_url": "https://app.eventeer.io/in/w/lst_7Qp2xK9vLmN3rT5wY8zA1b", "inserted_at": "2026-10-31T18:20:00.000000Z", "updated_at": "2026-10-31T18:20:00.000000Z" } }
curl -X POST https://app.eventeer.io/api/v1/listeners/lst_7Qp2xK9vLmN3rT5wY8zA1b/pause \ -H "Authorization: Bearer $EVENTEER_KEY"
{"data": {"id": "7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71", "key": "lst_7Qp2xK9vLmN3rT5wY8zA1b", "status": "paused", …}}
Credentials
Credentials authenticate token and HMAC listeners: token (Bearer token, lt_…), hmac_secret (signature secret, lhs_…, webhook listeners only) and device_token (dt_…, bound to one device). The plaintext is returned once, as secret in the creation response — store it right away; afterwards only its prefix is shown.
/api/v1/listeners/:key/credentials
Create a credential: kind (token | hmac_secret | device_token), optional name, and for device tokens the device_id (UUID) of a device of this listener.
/api/v1/listeners/:key/credentials/:id
Revoke a credential. Requests using it are rejected immediately.
writecurl -X POST https://app.eventeer.io/api/v1/listeners/lst_7Qp2xK9vLmN3rT5wY8zA1b/credentials \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"kind":"token","name":"checkout service"}'
{ "data": { "id": "6f1d3c2a-58b4-4e0f-9a7d-2c1b0e9f8a76", "kind": "token", "name": "checkout service", "prefix": "lt_9xQe2", "secret": "lt_9xQe2Lm7Vb4Np8Rt1Ks6Wd3Hy5Zc0JaUf2Gk7Pqs", "listener_id": "7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71", "device_id": null, "last_used_at": null, "revoked_at": null, "inserted_at": "2026-10-31T18:21:00.000000Z" } }
curl -X DELETE https://app.eventeer.io/api/v1/listeners/lst_7Qp2xK9vLmN3rT5wY8zA1b/credentials/6f1d3c2a-58b4-4e0f-9a7d-2c1b0e9f8a76 \ -H "Authorization: Bearer $EVENTEER_KEY"
Events
Stored events, newest first (unlike other lists): after (the previous page's next) returns older events. Filter by listener (key; an unknown key gives an empty page), device (device UUID), source (webhook | device | ws_source | api), from / to (ISO 8601, inclusive) and page with limit (1–200, default 50). Malformed filters → 422 validation_failed. Each event carries listener (key) and listener_id, device (external id) and device_id, and its body as data (JSON, or a string for text/plain). Events can also be ingested through the API with an ingest-scoped key (source: api); plan, spend cap, size, rate limits and the listener's allowed_ips apply as on webhook ingress.
/api/v1/events
List events (filters above, newest first).
read/api/v1/events/:id
Show one event with its payload.
read/api/v1/listeners/:key/events
Ingest an event through the API (source: api). Body = the JSON event payload (≤ 256 KB) → 202, or 200 with duplicate: true on an Idempotency-Key repeat.
curl "https://app.eventeer.io/api/v1/events?listener=lst_7Qp2xK9vLmN3rT5wY8zA1b&limit=1" \ -H "Authorization: Bearer $EVENTEER_KEY"
{ "data": [ { "id": "0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a3f", "listener": "lst_7Qp2xK9vLmN3rT5wY8zA1b", "listener_id": "7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71", "device": null, "device_id": null, "source": "webhook", "authenticated": true, "content_type": "application/json", "data": {"type": "order.created", "amount": 42.5}, "headers": {}, "idempotency_key": "order-1234", "size_bytes": 39, "remote_ip": "203.0.113.24", "received_at": "2026-10-31T18:21:04.123456Z" } ], "next": "0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a3f" }
curl -X POST https://app.eventeer.io/api/v1/listeners/lst_7Qp2xK9vLmN3rT5wY8zA1b/events \ -H "Authorization: Bearer $EVENTEER_INGEST_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1234" \ -d '{"type":"order.created","amount":42.5}'
{"data": {"id": "0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a3f", "duplicate": false}} # same Idempotency-Key again: # 200 {"data": {"id": "0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a3f", "duplicate": true}}
Devices
Devices connect to device listeners over a persistent WebSocket. They are created on first connection or pre-registered here, and addressed by their UUID id (external_id is the id the device connects with). Device messages (downlink) are best-effort and not stored: they reach the device only while it's online.
/api/v1/listeners/:key/devices
List the listener's devices (paginated) with status (online | offline), last_seen_at and last_connected_at.
/api/v1/listeners/:key/devices
Pre-register a device: external_id, name, metadata (422 not_device_listener on a non-device listener).
/api/v1/devices/:id
Update name or metadata (external_id is immutable → 422).
(or PUT)
/api/v1/devices/:id
Delete a device.
write/api/v1/devices/:id/messages
Send {"data": …} (any JSON value, ≤ 64 KB) to the open socket → 202, or 409 device_offline.
curl -X POST https://app.eventeer.io/api/v1/listeners/lst_2Fk9pD4sXq7Vn1Lc8Tb6Ry/devices \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"external_id":"pump-17","name":"Pump 17","metadata":{"site":"north"}}'
{ "data": { "id": "9b2e4f61-0c3d-4a8b-b7e5-1d6f2a9c8e30", "listener_id": "4d8a2c17-6e9b-4f30-a1d5-7b3c9e0f2a64", "external_id": "pump-17", "name": "Pump 17", "metadata": {"site": "north"}, "status": "offline", "last_seen_at": null, "last_connected_at": null, "inserted_at": "2026-10-31T18:22:00.000000Z", "updated_at": "2026-10-31T18:22:00.000000Z" } }
curl -X POST https://app.eventeer.io/api/v1/devices/9b2e4f61-0c3d-4a8b-b7e5-1d6f2a9c8e30/messages \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"data":{"cmd":"reboot"}}'
{"data": {"id": "msg_5Tg2Wq8Lp1Zx4Nc7Vb3Kd9"}} # device not connected: # 409 {"error": "device_offline", "message": "The device is not connected."}
Streams
A stream is a WebSocket feed of events for consumers (subscribe_url: wss://…/out/s/str_…), selected by listener_ids (listener UUIDs; empty = all listeners) and an optional single-condition filter ({"path": "$.type", "op": "eq", "value": …}; ops eq neq gt gte lt lte contains in exists). auth_mode is anonymous or token. With replay_allowed (default true), clients resume with ?since=<last event id>.
/api/v1/streams
List streams (paginated, ascending by id).
read/api/v1/streams
Create a stream: name, auth_mode, listener_ids, filter, replay_allowed.
/api/v1/streams/:key
Show a stream.
read/api/v1/streams/:key
Update name, auth_mode, listener_ids, filter, replay_allowed.
(or PUT)
/api/v1/streams/:key
Delete a stream (open sockets are closed).
write/api/v1/streams/:key/credentials
Create a subscription token (st_…, optional name): returned once as token, with the subscribe_url.
curl -X POST https://app.eventeer.io/api/v1/streams \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"orders feed","auth_mode":"token", "listener_ids":["7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71"], "filter":{"path":"$.type","op":"eq","value":"order.created"}}'
{ "data": { "id": "2b6f9d40-8c1a-4e57-b3d2-5a7e1f0c9b84", "key": "str_4Hc8nV2qWm6Rt1yZp0Lk3x", "name": "orders feed", "auth_mode": "token", "listener_ids": ["7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71"], "filter": {"path": "$.type", "op": "eq", "value": "order.created"}, "replay_allowed": true, "subscribe_url": "wss://app.eventeer.io/out/s/str_4Hc8nV2qWm6Rt1yZp0Lk3x", "plan_paused_at": null, "inserted_at": "2026-10-31T18:23:00.000000Z", "updated_at": "2026-10-31T18:23:00.000000Z" } }
curl -X POST https://app.eventeer.io/api/v1/streams/str_4Hc8nV2qWm6Rt1yZp0Lk3x/credentials \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"dashboard"}'
{ "data": { "id": "e3a71c58-0f2b-4d96-8c4e-1b7d5a2f9e03", "kind": "token", "name": "dashboard", "prefix": "st_Vq4Lm", "token": "st_Vq4Lm8Xc2Rb7Nk1Tz5Wp9Hd3Jf6Gs0Ya4Ue8Qn2L", "stream_key": "str_4Hc8nV2qWm6Rt1yZp0Lk3x", "subscribe_url": "wss://app.eventeer.io/out/s/str_4Hc8nV2qWm6Rt1yZp0Lk3x", "inserted_at": "2026-10-31T18:23:30.000000Z" } }
Destinations & deliveries
Destinations receive matching events (by listener_ids — listener UUIDs, empty = all — and an optional filter, as for streams) as signed outbound webhooks (X-Eventeer-Signature). Static headers are write-only: responses list only their header_names. When an update moves url to another origin (scheme, host or port), stored header values are cleared — unless re-entered in the same request — the response lists them in headers_cleared, and owners/admins are emailed. status is active, paused or disabled (with disabled_reason gone | failing). Failed deliveries are retried with backoff for ~24h (up to 8 retries), then marked dead; any failed or dead delivery can be retried by hand.
/api/v1/destinations
List destinations (paginated, ascending by id).
read/api/v1/destinations
Create a destination: name, url, listener_ids, filter, headers. The response includes the signing_secret, returned once.
/api/v1/destinations/:key
Show a destination.
read/api/v1/destinations/:key
Update name, url, listener_ids, filter, headers, and status: "paused" pauses, "active" resumes or re-enables a disabled destination (403 plan_limit if the plan paused it).
(or PUT)
/api/v1/destinations/:key
Delete a destination.
write/api/v1/destinations/:key/rotate-secret
Rotate the signing secret: the new signing_secret is returned once.
/api/v1/destinations/:key/deliveries
Delivery log, newest first by time (next is an opaque cursor), optionally filtered by status (pending | succeeded | failed | dead).
/api/v1/deliveries/:id/retry
Retry a failed or dead delivery now → 202 with the delivery back to pending (422 not_retryable / destination_disabled otherwise).
curl -X POST https://app.eventeer.io/api/v1/destinations \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"billing hook","url":"https://example.com/hooks/eventeer", "listener_ids":["7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71"], "headers":{"Authorization":"Bearer s3cr3t"}}'
{ "data": { "id": "a54c0e72-9b3d-4f18-8e6a-3c2d7f1b0e95", "key": "dst_8Wm3Kq1Zp6Vt9Nb2Lc5Xr4", "name": "billing hook", "url": "https://example.com/hooks/eventeer", "listener_ids": ["7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71"], "filter": null, "header_names": ["Authorization"], "status": "active", "disabled_reason": null, "failing_since": null, "last_success_at": null, "plan_paused_at": null, "signing_secret": "dsec_3Jd8Lq2Vn6Rb9Tk1Xp4Zm7Hc5Wf0Ys2Ga8Ue4No1", "inserted_at": "2026-10-31T18:24:00.000000Z", "updated_at": "2026-10-31T18:24:00.000000Z" } }
curl -X PATCH https://app.eventeer.io/api/v1/destinations/dst_8Wm3Kq1Zp6Vt9Nb2Lc5Xr4 \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"status":"paused"}'
{"data": {"key": "dst_8Wm3Kq1Zp6Vt9Nb2Lc5Xr4", "status": "paused", …}}
curl "https://app.eventeer.io/api/v1/destinations/dst_8Wm3Kq1Zp6Vt9Nb2Lc5Xr4/deliveries?status=failed&limit=1" \ -H "Authorization: Bearer $EVENTEER_KEY"
{ "data": [ { "id": "1c7e9a24-5b3f-4d6e-8a1b-0f2c3d4e5f60", "destination_id": "a54c0e72-9b3d-4f18-8e6a-3c2d7f1b0e95", "destination_key": "dst_8Wm3Kq1Zp6Vt9Nb2Lc5Xr4", "event_id": "0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a3f", "status": "failed", "attempts": 3, "last_status_code": 503, "last_error": "HTTP 503", "last_attempt_at": "2026-10-31T18:30:00.000000Z", "next_attempt_at": "2026-10-31T18:40:00.000000Z", "delivered_at": null, "inserted_at": "2026-10-31T18:21:04.200000Z", "updated_at": "2026-10-31T18:30:00.000000Z" } ], "next": null }
curl -X POST https://app.eventeer.io/api/v1/deliveries/1c7e9a24-5b3f-4d6e-8a1b-0f2c3d4e5f60/retry \ -H "Authorization: Bearer $EVENTEER_KEY"
{"data": {"id": "1c7e9a24-5b3f-4d6e-8a1b-0f2c3d4e5f60", "status": "pending", "attempts": 3, …}}
Metrics
Metrics aggregate events into time series: count, or sum / avg / min / max of a numeric value_path (a JSON path such as $.amount) in the payload, over listener_ids (listener UUIDs; empty = all) with an optional single-condition filter (as for streams). plan_paused is true when a plan downgrade paused the metric.
/api/v1/metrics
List metrics (paginated, ascending by id).
read/api/v1/metrics
Create a metric: name, aggregation, value_path (required unless count), unit, listener_ids, filter.
/api/v1/metrics/:id
Show a metric.
read/api/v1/metrics/:id
Update a metric (same fields).
(or PUT)
/api/v1/metrics/:id
Delete a metric.
write/api/v1/metrics/:id/series
Time series: from, to (ISO 8601 with offset, or unix seconds; default the last hour), step (1m | 5m | 1h | 1d; default picked from the range; at most 1500 points). Returned top-level, not under data; empty buckets are omitted.
curl -X POST https://app.eventeer.io/api/v1/metrics \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Order total","aggregation":"sum","value_path":"$.amount","unit":"USD", "listener_ids":["7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71"], "filter":{"path":"$.type","op":"eq","value":"order.created"}}'
{ "data": { "id": "3a9c5e71-2d4b-4f68-9e0a-7b1c2d3e4f50", "name": "Order total", "aggregation": "sum", "value_path": "$.amount", "unit": "USD", "listener_ids": ["7c1e4b92-3a5d-4f60-8b2e-9d0a1c3e5f71"], "filter": {"path": "$.type", "op": "eq", "value": "order.created"}, "plan_paused": false, "inserted_at": "2026-10-31T18:25:00.000000Z", "updated_at": "2026-10-31T18:25:00.000000Z" } }
curl "https://app.eventeer.io/api/v1/metrics/3a9c5e71-2d4b-4f68-9e0a-7b1c2d3e4f50/series?from=2026-10-31T17:00:00Z&to=2026-10-31T18:00:00Z&step=1m" \ -H "Authorization: Bearer $EVENTEER_KEY"
{ "metric_id": "3a9c5e71-2d4b-4f68-9e0a-7b1c2d3e4f50", "aggregation": "sum", "from": "2026-10-31T17:00:00Z", "to": "2026-10-31T18:00:00Z", "step": "1m", "points": [ {"t": "2026-10-31T17:00:00Z", "value": 127.5, "count": 3}, {"t": "2026-10-31T17:01:00Z", "value": 42.5, "count": 1} ] }
Alert rules
Alert rules watch a metric (threshold: the value over window_min minutes gt/gte/lt/lte a threshold) or a listener, optionally one device (absence: no events for window_min minutes). Firing opens an incident and notifies the rule's channels: {"type": "email", "to": [emails of members]} and/or {"type": "webhook", "destination_id": "<destination UUID>"} — at least one is required. state is ok or firing; kind is ignored on update.
/api/v1/alert-rules
List alert rules (paginated, ascending by id).
read/api/v1/alert-rules
Create a rule: name, kind, metric_id + operator + threshold (threshold) or listener_id + optional device_id (absence), window_min (1–1440, default 5), cooldown_min (0–10080, default 0), channels, enabled.
/api/v1/alert-rules/:id
Show a rule with its state, open_incident_id and last_value.
/api/v1/alert-rules/:id
Update a rule (e.g. enabled: false).
(or PUT)
/api/v1/alert-rules/:id
Delete a rule.
write/api/v1/alert-rules/:id/incidents
The rule's incidents (paginated, ascending by id).
readcurl -X POST https://app.eventeer.io/api/v1/alert-rules \ -H "Authorization: Bearer $EVENTEER_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"High temperature","kind":"threshold", "metric_id":"6b0d3f82-4e1c-4a97-b5d8-2f9e0a1c7d36", "operator":"gt","threshold":80,"window_min":5, "channels":[{"type":"email","to":["[email protected]"]}]}'
{ "data": { "id": "5e2b8d14-7a3c-4f91-b6e0-2c9d1a8f7e63", "name": "High temperature", "kind": "threshold", "summary": "avg Temperature over 5m > 80", "metric_id": "6b0d3f82-4e1c-4a97-b5d8-2f9e0a1c7d36", "operator": "gt", "threshold": 80.0, "listener_id": null, "device_id": null, "window_min": 5, "cooldown_min": 0, "channels": [{"type": "email", "to": ["[email protected]"]}], "enabled": true, "state": "ok", "open_incident_id": null, "last_value": null, "last_evaluated_at": null, "plan_paused": false, "inserted_at": "2026-10-31T18:26:00.000000Z", "updated_at": "2026-10-31T18:26:00.000000Z" } }
curl https://app.eventeer.io/api/v1/alert-rules/5e2b8d14-7a3c-4f91-b6e0-2c9d1a8f7e63/incidents \ -H "Authorization: Bearer $EVENTEER_KEY"
{ "data": [ { "id": "c8f1a3e6-2d7b-4950-9e4c-0a6b8d2f1e57", "alert_rule_id": "5e2b8d14-7a3c-4f91-b6e0-2c9d1a8f7e63", "open": true, "opened_at": "2026-10-31T19:02:00.000000Z", "resolved_at": null, "trigger_value": 83.4, "notified_at": "2026-10-31T19:02:01.000000Z" } ], "next": null }
Dashboards
Dashboards are read-only through the API; build and edit them in the app. system marks the built-in Overview. Each widget has a type (timeseries | stat | event_feed), the metric_ids it reads, its remaining config as options, and a position (x, y, w, h) on a 12-column grid. default_range is 15m | 1h | 6h | 24h | 7d | 30d; refresh is live | 1m | off.
/api/v1/dashboards
List dashboards with their widgets (paginated, ascending by id).
read/api/v1/dashboards/:id
Show a dashboard with its widgets.
readcurl https://app.eventeer.io/api/v1/dashboards/8d4f2a61-9c3e-4b7a-a5d0-6e1f2b3c4d58 \ -H "Authorization: Bearer $EVENTEER_KEY"
{ "data": { "id": "8d4f2a61-9c3e-4b7a-a5d0-6e1f2b3c4d58", "name": "Pumps", "system": false, "default_range": "1h", "refresh": "live", "plan_paused": false, "widgets": [ { "id": "f2c7b9e4-1a6d-4083-b5e2-9d4a0c3f8b71", "type": "timeseries", "title": "Temperature", "metric_ids": ["6b0d3f82-4e1c-4a97-b5d8-2f9e0a1c7d36"], "options": {}, "position": {"x": 0, "y": 0, "w": 6, "h": 4} }, { "id": "0e9a4d27-6b3f-4c18-a7e5-2d1b8f6c4a90", "type": "stat", "title": "Latest temperature", "metric_ids": ["6b0d3f82-4e1c-4a97-b5d8-2f9e0a1c7d36"], "options": {"stat": "last"}, "position": {"x": 6, "y": 0, "w": 3, "h": 2} } ], "inserted_at": "2026-10-31T18:27:00.000000Z", "updated_at": "2026-10-31T18:27:00.000000Z" } }
Usage
Usage of the current billing period: plan (code, name, or null without a plan), status, plan_valid and blocked (null, no_plan or spend_cap); per metered dimension in dimensions its used, quota and overage_cents; the total overage_cents, spend_cap_cents and spend_cap_source (plan default or custom); every hard limit in limits as {used, limit} (limit is null when unlimited); and the per-second rates in effect in rate_limits — ingest_rate_per_listener and api_rate_per_key — as {limit, custom} (custom is true when a limit was adjusted for your organization).
/api/v1/usage
Current period usage, limits and spend cap.
readcurl https://app.eventeer.io/api/v1/usage -H "Authorization: Bearer $EVENTEER_KEY"
{ "data": { "plan": {"code": "growth", "name": "Growth"}, "status": "active", "plan_valid": true, "blocked": null, "period_start": "2026-10-01T00:00:00.000000Z", "period_end": "2026-11-01T00:00:00.000000Z", "currency": "usd", "dimensions": { "events_ingested": {"used": 1250000, "quota": 5000000, "overage_cents": 0}, "events_delivered": {"used": 2400000, "quota": 10000000, "overage_cents": 0}, "connection_minutes": {"used": 310000, "quota": 10000000, "overage_cents": 0} }, "overage_cents": 0, "spend_cap_cents": 25000, "spend_cap_source": "plan", "bytes_ingested": 512000000, "limits": { "listeners": {"used": 4, "limit": 25}, "connections": {"used": 12, "limit": 500}, "streams_and_destinations": {"used": 3, "limit": 25}, "metrics": {"used": 6, "limit": 50}, "alert_rules": {"used": 2, "limit": 50}, "dashboards": {"used": 2, "limit": 20}, "members": {"used": 3, "limit": 10} }, "rate_limits": { "ingest_rate_per_listener": {"limit": 100, "custom": false}, "api_rate_per_key": {"limit": 100, "custom": false} } } }
Ingress & sockets
Real-time traffic doesn't go through /api/v1: events come in through listener URLs and go out through stream sockets, authenticated per listener / stream.
/in/w/:listener_key
Webhook ingress. JSON, text/* or form bodies up to 256 KB.
/in/d/:listener_key?device_id=…
Device connection (persistent WebSocket, JSON text frames).
/out/s/:stream_key?token=…&since=…
Stream subscription: replays stored events after since, then live.
Listener and stream auth_mode:
anonymous
(the unguessable key is enough), token
(Authorization: Bearer …
or ?token=
for WebSocket clients — a credential, a device token or an API key with the
ingest
scope) or hmac
(webhooks signed with X-Eventeer-Signature). Streams with
replay_allowed
resend stored events after ?since=<event id>
(up to 1,000), then a live
marker.
HTTP responses (webhook ingress)
-
202
stored —
{"id": "…", "duplicate": false} -
200
idempotency hit (
Idempotency-Keyheader) —"duplicate": true - 401 unauthorized — token/HMAC missing or wrong on an authenticated listener
- 402 plan_required / spend_cap_reached
- 404 not_found — unknown listener key
- 413 payload_too_large — body over 256 KB
- 415 unsupported content type
- 423 listener_paused
-
429
rate_limited (+
Retry-After) or connection_limit before a WS upgrade
WebSocket close codes
- 4401 unauthorized
- 4402 plan required or spend cap reached
-
4408
slow consumer (stream) — reconnect with
since - 4409 replaced by a newer connection with the same device_id
- 4423 listener paused
- 4429 rate limited (sustained)
Upgrades are refused with HTTP 401 / 402 / 429 before the socket opens.
curl -X POST https://app.eventeer.io/in/w/lst_7Qp2xK9vLmN3rT5wY8zA1b \ -H "Authorization: Bearer lt_…" \ -H "Idempotency-Key: order-1234" \ -H "Content-Type: application/json" \ -d '{"type":"order.created","amount":42.5}' # 202 {"id":"0192f3a4-7b1e-7c55-9a3e-5d2f8b6c1a3f","duplicate":false}
X-Eventeer-Signature: t=1761930000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e7… # v1 = hex(HMAC_SHA256(secret, "#{t}.#{raw_body}")), rejected if |now - t| > 300s # With an Idempotency-Key also send v2 (the key is then covered by the signature): # v2 = hex(HMAC_SHA256(secret, "v2.#{t}.#{bytesize(key)}.#{key}.#{raw_body}")) # A signature is accepted once: re-sending it is a replay (401), except a retry # with the same Idempotency-Key, which returns the original event (200, duplicate).
// connect: wss://app.eventeer.io/in/d/lst_…?device_id=pump-17&token=dt_… // device → Eventeer {"type":"event","ref":"r1","data":{"temp":21.4}} {"type":"ping"} // Eventeer → device {"type":"ack","ref":"r1","id":"0192f3a4-…"} {"type":"message","id":"msg_…","data":{"cmd":"reboot"}}
const ws = new WebSocket(`wss://app.eventeer.io/out/s/${key}?token=${token}&since=${lastId}`) ws.onmessage = (m) => { const f = JSON.parse(m.data) if (f.type === "event") lastId = f.id // reconnect with since=lastId }