API Reference

The Nightjar REST surface, one watcher per stream.

Three resource families: services, anomalies, and incidents. Every endpoint is owner-scoped — sign in first, then call with the bearer session cookie. Errors use HTTP status codes; a 404 also covers rows you don’t own, so probing can’t distinguish “missing” from “not yours.”

Bearer session cookie · owner-scoped reads

Index

All endpoints

MethodPathDescription
GET
/api/connectionsReturn every connection the signed-in user owns.
POST
/api/connectionsAdd a new BullMQ source for the signed-in user.
POST
/api/connections/[id]/testBest-effort TCP reachability check for a stored connection.
GET
/api/anomaliesRecent anomalies for the caller’s services, filtered by time window.
GET
/api/incidents/[id]A single anomaly plus its event trace and metric buckets.

Services

Register the BullMQ sources Nightjar should watch. One connection per redisUrl — each connection lists the queues to monitor on it.

List service connections

Return every connection the signed-in user owns.

GET
/api/connections
Session cookie (better-auth)

Returns the connections the caller registered (one row per BullMQ source). Ordered by createdAt descending so the most recently added service is first. Owner-scoped — never returns another tenant’s services even by accident.

Response · 200

A list response shape (ServiceConnectionList).

{
  "connections": [
    {
      "id": "rls8m2z4k9w0",
      "name": "checkout-worker",
      "redisUrl": "redis://redis.internal:6379",
      "queues": ["billing", "invoices", "exports"],
      "createdAt": "2026-04-12T09:31:08.124Z"
    },
    {
      "id": "cm4p7t1ne8qr",
      "name": "inbox-processor",
      "redisUrl": "redis://10.0.4.21:6379",
      "queues": ["mail-queue", "digest-queue"],
      "createdAt": "2026-03-02T17:55:44.901Z"
    }
  ]
}

Errors

Failure modes

401

No session cookie present (or session expired).

{ "error": "Unauthorized" }

Register a service connection

Add a new BullMQ source for the signed-in user.

POST
/api/connections
Session cookie (better-auth)

Register a Redis instance plus the queues Nightjar should watch on it. The redisUrl must start with redis:// or rediss://. queuesRaw is a comma-separated list — duplicated or blank entries are trimmed silently. Owner is the session user, never a value from the request body.

Body

Request body

{
  "name": "checkout-worker",
  "redisUrl": "redis://redis.internal:6379",
  "queuesRaw": "billing, invoices, exports"
}

Response · 201

The persisted connection (ServiceConnection).

{
  "id": "rls8m2z4k9w0",
  "name": "checkout-worker",
  "redisUrl": "redis://redis.internal:6379",
  "queues": ["billing", "invoices", "exports"],
  "createdAt": "2026-04-12T09:31:08.124Z"
}

Errors

Failure modes

400

Field-level validation failed.

{
  "errors": {
    "name": "Service name is required.",
    "redisUrl": "Redis URL must start with redis:// or rediss://.",
    "queuesRaw": "List at least one BullMQ queue (comma-separated)."
  }
}

Test a service connection

Best-effort TCP reachability check for a stored connection.

POST
/api/connections/[id]/test
Session cookie (better-auth)

Open a TCP socket to the host/port derived from the stored redisUrl and time out after ~2 s. Useful before/during registration to confirm the watcher will be able to reach Redis. Does not authenticate Redis — only confirms the host is reachable on the network.

Response · 200

The connection is reachable.

{ "ok": true }

Errors

Failure modes

404

No connection with that id for the signed-in user.

{ "ok": false, "error": "Not found." }
502

TCP connect failed or timed out (Redis unreachable, DNS error, TLS error, etc.).

{ "ok": false, "error": "Timed out after 2000ms." }

Anomalies

Anomalies detected by the watcher cron. Narrow by time window, and by the connection that owns the affected queues.

List detected anomalies

Recent anomalies for the caller’s services, filtered by time window.

GET
/api/anomalies
Session cookie (better-auth)

Returns up to 100 recent anomalies joined back to the caller’s services by streamKey. A row whose streamKey matches one of the caller’s registered queues is enriched with that service’s name and queue; otherwise serviceName and queueName come back as null. The list is not paginated — the time-window cap is the only filter applied besides the optional service id.

Query params

Request

NameDescription
range

Default: 7d

Look-back window. One of 24h, 7d, or 30d. Anything else silently coerces to 7d.
serviceOne or more comma-separated connection ids owned by the caller. Narrows results to the union of those services’ queues. An id the caller does not own is ignored (same effect as omitting the filter).

Response · 200

A list response shape (AnomalyList).

{
  "total": 12,
  "items": [
    {
      "id": "ans_k0q2p1n9t8m4",
      "streamKey": "bull:billing:events",
      "detectorType": "consumer_lag_spike",
      "severity": "warn",
      "summary": "checkout-worker billing consumer lag crossed 2 000 ms for 90 s.",
      "rootCause": "Downstream invoices worker is processing ~6 s slower than billed events are produced. Sustained lag, not a burst.",
      "snippet": {
        "lagMs": 2418,
        "consumer": "invoices-1",
        "windowSec": 90
      },
      "slackPosted": true,
      "detectedAt": "2026-04-18T14:22:11.000Z",
      "serviceName": "checkout-worker",
      "queueName": "billing"
    }
  ]
}

Errors

Failure modes

401

No session cookie present (or session expired).

{ "error": "Unauthorized" }

Incidents

Incident detail — the anomaly plus the surrounding event trace and a 10-bucket metric grid for the 20-minute window around detectedAt.

Get incident detail

A single anomaly plus its event trace and metric buckets.

GET
/api/incidents/[id]
Session cookie (better-auth)

Returns the parent anomaly enriched with lagMsAtIncident, the surrounding event trace (±10 minutes around detectedAt, merged ±100 rows from each side), and 10 evenly-sized 2-minute metric buckets filling the same 20-minute window. If the anomaly’s streamKey doesn’t match any queue the caller owns, the response is 404 — never 403 — so probing users can’t tell the row exists.

Response · 200

A detail payload (IncidentDetail).

{
  "anomaly": {
    "id": "ans_k0q2p1n9t8m4",
    "streamKey": "bull:billing:events",
    "detectorType": "consumer_lag_spike",
    "severity": "warn",
    "summary": "checkout-worker billing consumer lag crossed 2 000 ms for 90 s.",
    "rootCause": "Downstream invoices worker is processing ~6 s slower than billed events are produced. Sustained lag, not a burst.",
    "snippet": {
      "lagMs": 2418,
      "consumer": "invoices-1",
      "windowSec": 90
    },
    "slackPosted": true,
    "detectedAt": "2026-04-18T14:22:11.000Z",
    "serviceName": "checkout-worker",
    "queueName": "billing",
    "lagMsAtIncident": 2418
  },
  "events": [
    {
      "entryId": "168-0",
      "recordedAt": "2026-04-18T14:22:08.012Z",
      "payload": { "type": "charge.succeeded", "lagMs": 2310 },
      "error": false,
      "lagMs": 2310
    }
  ],
  "metrics": {
    "totalEvents": 187,
    "totalErrors": 3,
    "windowStart": "2026-04-18T14:12:11.000Z",
    "windowEnd": "2026-04-18T14:32:11.000Z",
    "buckets": [
      {
        "label": "-10 min",
        "startMs": 1776279131000,
        "endMs": 1776279251000,
        "eventCount": 12,
        "errorCount": 0,
        "lagMs": 412,
        "isIncidentBucket": false
      }
    ]
  }
}

Errors

Failure modes

404

No anomaly with that id, or the row belongs to a service the caller doesn’t own (returned as 404 to avoid leaking existence).

{ "error": "Not Found" }

Want a key? Talk to us.

You don’t need to start a watcher on a single stream. We do.

Nightjar is HITL-gated and reversible from day one. Tell us where your Redis Streams live and we’ll spin up a watcher — usually within a day. No SDK to install, no agent to bolt onto the host.