Events (SSE)

A persistent event stream that delivers real-time reservation changes to your application.

Endpoint

GET /api/v1/events
X-Api-Key: YOUR_API_KEY

Connect once; the server pushes events as they happen. Uses the Server-Sent Events protocol — a plain HTTP response with Content-Type: text/event-stream. Each event is tenant-scoped: you only receive events from your own tenant.

Browser — native EventSource

The browser's built-in EventSource API handles reconnection automatically. Dashboard SSE connections are authenticated via cookie session and do not count against your per-tier SSE connection limit.

const es = new EventSource("/api/v1/events");

es.addEventListener("RESERVED", (e) => {
  const evt = JSON.parse(e.data);
  console.log("Reserved:", evt.resource, "by", evt.requester);
});

es.addEventListener("UNRESERVED", (e) => {
  const evt = JSON.parse(e.data);
  console.log("Released:", evt.resource);
});

es.addEventListener("RESERVATION_EXPIRED", (e) => {
  const evt = JSON.parse(e.data);
  console.log("TTL expired:", evt.resource);
});

es.onerror = () => {
  // Browser reconnects automatically — no action needed here
};
Named events: Each SSE frame uses the event type as the named event (e.g. event: RESERVED). Use addEventListener("RESERVED", ...) rather than onmessage to receive typed events.

Server-side — Node.js

import EventSource from "eventsource"; // npm i eventsource

const es = new EventSource("https://cooplocker.com/api/v1/events", {
  headers: { "X-Api-Key": "YOUR_API_KEY" }
});

es.addEventListener("RESERVED", (e) => {
  const evt = JSON.parse(e.data);
  console.log(evt);
});

Server-side — Python

import sseclient, requests

resp = requests.get(
    "https://cooplocker.com/api/v1/events",
    headers={"X-Api-Key": "YOUR_API_KEY"},
    stream=True
)

for event in sseclient.SSEClient(resp).events():
    if event.event == "RESERVED":
        print("reserved:", event.data)

Keepalive

The server sends a : keepalive comment every 15 seconds to prevent proxy and load-balancer timeouts from dropping idle connections. These are invisible to your event handlers.

Event types

TypeWhen emitted
RESERVEDA resource was successfully reserved.
UNRESERVEDA resource was released — either by an explicit unreserve call or by TTL expiry.
RESERVATION_EXPIREDA TTL reservation was swept by the server expiry loop.
RESERVE_FAILEDA reserve attempt was rejected (e.g. ALREADY_HELD).
UNRESERVE_FAILEDAn unreserve attempt failed (e.g. invalid token).
TRANSACTION_APPLIEDA multi-resource transaction completed successfully.
TRANSACTION_ROLLED_BACKA transaction was rolled back (ALL_OR_NONE conflict).
RESETAll reservations for the tenant were cleared.
TIER_LIMIT_REACHEDA rate or capacity limit was hit. Includes limitType and limit fields.

Event payload shape

All events share this base shape. Fields that don't apply to a given event type are null.

{
  "type":        "RESERVED",
  "tenantId":    "...",          // your tenant ID
  "resource":    "job-42",       // resource name
  "requester":   "worker-1",     // who made the call
  "explanation": "ACQUIRED",     // outcome detail
  "resourceType":"UserDefined",  // tag from your reserve call
  "apiKeyLabel": "worker-prod",  // key label, if set
  "requestType": "RESERVE",
  "timestamp":   "2026-05-13T12:00:00Z",

  // Transaction events only:
  "method":  "ALL_OR_NONE",
  "changes": [
    {"resource":"lock-1","requester":"svc-a","requestType":"RESERVE","explanation":"ACQUIRED"}
  ],

  // Tier limit events only:
  "limitType": "ConcurrentReservations",
  "limit":     500
}

Connection limits

API key and JWT authenticated SSE connections count against your tier limit. Dashboard connections (cookie session) are excluded from the count.

TierSSE connections (API key)
Free1
Starter5
Pro20
EnterpriseUnlimited

Exceeding the limit returns HTTP 429 with an upgradeRequired: true body rather than opening a stream.

Reconnection

If the connection drops, reconnect and re-subscribe. There is no event replay — events emitted while disconnected are not buffered. For durable event history, poll GET /api/v1/Reservation/activeReservations on reconnect to resync state before re-subscribing.