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.”
Index
All endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/connections | Return every connection the signed-in user owns. |
POST | /api/connections | Add a new BullMQ source for the signed-in user. |
POST | /api/connections/[id]/test | Best-effort TCP reachability check for a stored connection. |
GET | /api/anomalies | Recent 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.
/api/connectionsReturns 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
No session cookie present (or session expired).
{ "error": "Unauthorized" }Register a service connection
Add a new BullMQ source for the signed-in user.
/api/connectionsRegister 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
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.
/api/connections/[id]/testOpen 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
No connection with that id for the signed-in user.
{ "ok": false, "error": "Not found." }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.
/api/anomaliesReturns 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
| Name | Description |
|---|---|
rangeDefault: | Look-back window. One of 24h, 7d, or 30d. Anything else silently coerces to 7d. |
service | One 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
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.
/api/incidents/[id]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
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.