docs
API

Beacons

List, read and count beacons with an API key, and see which routes promote, hold, dismiss and regroup them, which need a signed-in session.

On for every workspace. The three read routes take an API key or an OAuth token. Every route that changes a beacon needs a signed-in session token instead, see Session-only beacon routes.

A beacon is a ranked collection of related signals, and it is the unit a decision is made on: promote it to a spec, hold it, or dismiss it. Its rankScore is one integer from 0 to 100, blended from severity (0.35), reach (0.30), producer weight (0.20) and recency (0.15). How nima decides has the full formula and the thresholds behind it.

Read beacons with an API key

Three beacon routes accept an API key: list, get one, and count facets.

MethodPathScope
GET/v1/beaconssignals:read
GET/v1/beacons/{id}signals:read
GET/v1/beacons/facetssignals:read

Every other beacon route carries no scope. Send a key or an OAuth token to one of them and you get a 403 reading This endpoint does not accept OAuth access tokens, whatever scopes the key holds. There is no MCP tool that promotes, holds, dismisses, edits, creates or deletes a beacon, and none that removes member signals.

Every call takes your key as a bearer token and is scoped to the workspace that key belongs to:

curl https://api.getnima.io/v1/beacons \
  -H "Authorization: Bearer $NIMA_API_KEY"

See Authentication for how to create one.

List beacons

GET/v1/beacons
scopesignals:readMCP toollist_beacons

With no parameters you get every beacon in the workspace, held and dismissed ones included, highest rank first.

statusstring

One or more of forming, ready, held, promoted, dismissed. Comma-separated (status=ready,held) or repeated (status[]=ready&status[]=held).

slicestring

One of ready, started_by_nima, forming, held, in_progress, shipped, dismissed. Lifecycle position rather than raw status: it splits promoted by whether the spec shipped, and lifts out what nima promoted by itself. Every beacon sits in exactly one slice.

searchstring

Case-insensitive substring of the title or the summary. A value shaped like a key (BCN-142 or bcn142) is an exact key lookup instead.

sortBystring

rank (default, highest first), newest, or evidence (most member signals first). Ties break by id, descending.

pageinteger

Defaults to 1.

limitinteger

Defaults to 20, clamped to 100. The parameter is limit, not pageSize.

filterstring

A JSON filter expression, described under Filter beacons with a JSON expression. Applied on top of the parameters above, never instead of them.

Two of these fail differently, and the difference bites. An unrecognised slice is a 400. An unrecognised status is dropped silently, so ?status=in_progress returns the unfiltered list rather than an error, and so does an unrecognised sortBy. page and limit fall back to their defaults when they are not numbers.

page times limit cannot exceed 3000. Past that you get a 400 reading Cannot page beyond 3000 rows. Narrow the filter instead of paging deeper. There is no cursor: narrow with status, slice or filter instead.

meta carries no cursor and no countMode: totalItems is always an exact count of what matched.

The list_beacons MCP tool calls this route with a smaller set of inputs; see Pipeline tools.

Errors from listing beacons

StatuserrorWhen
400VALIDATION_ERRORAn unrecognised slice, a malformed filter, or page times limit past 3000.

Get a beacon

GET/v1/beacons/{id}
scopesignals:readMCP toolget_beacon

id is the UUID from a list response, not the BCN-142 key. A beacon that belongs to another workspace answers exactly like one that does not exist.

iduuidrequired

Path parameter. The beacon id from a list response.

The response carries no member signals. previewSignalIds gives at most three member ids, highest priority first, and you read each one with GET /v1/signals/{id}. There is no key-reachable route that returns every member of a beacon, and the signal list has no per-beacon filter, so for a beacon with 40 members you can reach 3. evidenceCount tells you how many there are.

An id that is not a UUID is not validated at the edge. It reaches the database and comes back as a 500 with an incidentId, not a 400. Check the shape before you call.

Errors from getting a beacon

StatuserrorWhen
404RESOURCE_NOT_FOUNDNo such beacon in this workspace.
500INTERNAL_SERVER_ERRORAn id that is not a UUID.

Count the facets

GET/v1/beacons/facets
scopesignals:read

How many beacons each status and each slice would hold, for the same scope your list is using. Use it for the number beside each status or slice.

fieldsstring

Comma-separated facets to count: status, slice, or both. Defaults to both. An unknown name is a 400 that names what is allowed.

filterstring

The same filter expression the list is using.

statusstring

The same flat parameter the list is using.

slicestring

The same flat parameter the list is using.

searchstring

The same flat parameter the list is using.

Send those three, or the numbers beside a narrowed list are counts of the whole workspace.

Counts drill sideways when the filter's root combinator is and; with an or root every facet is counted against the whole filter. For each facet, that facet's own conditions are removed from filter and every other condition is kept, so ticking a second value in one facet widens the result instead of emptying it. The flat status, slice and search parameters are not removed, so pinning slice=ready makes every other slice count 0.

Every value the schema declares is present, zero-filled. arrays is always null for beacons, which have no array-valued fields. Counting has a 500ms budget; blow it and you get "mode": "timeout" with total and facets set to null rather than wrong numbers. Results are cached for 30 seconds, and a timeout for 10.

Errors from counting facets

StatuserrorWhen
400VALIDATION_ERRORAn unknown name in fields, or a malformed filter.

Beacon object fields

iduuid

The beacon id.

organizationIdstring

The workspace the beacon belongs to.

keystring

BCN-142. Sequential per workspace and never reused.

titlestring

The beacon title.

summarystring or null

One line stating the ask. Null until it is written.

statusenum

forming, ready, held, promoted or dismissed.

rankScoreinteger

0 to 100.

rankBreakdownobject or null

The four terms as they stood at computedAt. Null until the first ranking pass has run.

heldReasonstring or null

Set when the beacon is held. Un-holding leaves it in place unless you send heldReason: null.

dispositionenum or null

fix means a plain defect with no product decision left; decide means a person chooses. nima starts the spec for a fix beacon by itself only when the nima team has turned auto-promote on for the platform, and it is off today. Null on beacons grouped by hand.

autoPromotedAtISO 8601 or null

UTC. Set only when nima promoted the beacon itself.

formedAtISO 8601 or null

UTC. Null until the beacon has formed: when nima placed it, when it carries an urgent incident, or when a hand-grouped beacon reached 3 pieces of evidence. A hand-grouped beacon can have formedAt set while status is still forming.

promotedSpecIdstring or null

The spec document id. Set when status is promoted.

promotionNotificationIdstring or null

Non-null only while a promotion is in flight.

previewSignalIdsstring[]

At most three member signal ids, highest priority first.

pmNotesstring or null

The written take on scope, priorities and exclusions. It is also the brief the spec writer works from.

evidenceCountinteger

Member signals.

sourceCountsobject

Count per producer: slack, github, manual, hubspot, web_performance, granola, posthog, sentry, linear, clickup, webhook. An absent key means zero. These count evidence events, not member signals, so they can sum higher than evidenceCount.

stakeholderCountinteger

Distinct stakeholders across the members. This is the reach input.

latestEvidenceAtISO 8601 or null

UTC timestamp of the newest evidence.

accountRollupobject

The accounts behind the beacon, for display only; rank does not use it. accountCount, customerCount, arrAtStakeUsd, renewalsIn90Days (renewals in the next 90 days), lostDealsUsd, lostDealsCount, and accounts (at most 10, highest ARR first). A beacon with no attributed account gets zero counts. The field is omitted only when the account read fails.

createdAtISO 8601

UTC.

updatedAtISO 8601

UTC.

rankBreakdown is stored, not recomputed on read, so it is what nima knew at computedAt:

severity, reach, weight, recencyobject

Each is { "value": 0..1, "coefficient": number }. The coefficient is the one that was in force when the score was computed.

scoreinteger

0 to 100. Mirrors rankScore.

computedAtISO 8601

UTC.

Filter beacons with a JSON expression

filter is one JSON object, URL-encoded, containing a combinator (and or or) and a conditions array. A condition is { "field", "operator", "value" }; a nested group is another object of the same shape.

FieldTypeOperators
statusenumin, not_in, is_set, is_not_set
sliceenumin, not_in, is_set, is_not_set
titletextcontains, not_contains, is_set, is_not_set
pmNotestextcontains, not_contains, is_set, is_not_set
rankScorenumbereq, neq, gt, gte, lt, lte, between
evidenceCountnumbereq, neq, gt, gte, lt, lte, between
createdAtdategt, gte, lt, lte, between, in_last, is_set, is_not_set
formedAtdatesame as createdAt
updatedAtdatesame as createdAt
hasSpecrelationexists, not_exists

rankScore compares against the same integer 0 to 100 the object carries. Date values are ISO 8601 UTC strings, between takes a two-element array, and in_last takes { "amount": 14, "unit": "day" } with a unit of day, week or month. The four operators that ask about presence (is_set, is_not_set, exists, not_exists) take no value.

curl -G https://api.getnima.io/v1/beacons \
  -H "Authorization: Bearer $NIMA_API_KEY" \
  --data-urlencode 'filter={"combinator":"and","conditions":[{"field":"status","operator":"in","value":["ready"]},{"field":"rankScore","operator":"gte","value":70},{"field":"hasSpec","operator":"not_exists"}]}'

The limits are 3 levels of nesting, 32 conditions across the whole tree, 50 values in one in or not_in, and 200 characters in a text value. Breaking one is a 400 that says which. So is a filter that is not a single JSON string, is not valid JSON, or names a field or operator that is not in the table above.

Session-only beacon routes

Every route that changes a beacon needs a signed-in session token (the JWT the nima app sends), not an API key or OAuth token. The admin panel has no beacon page, so in practice these are called by the nima team or with a session token you hold. All of them are scoped to the caller's workspace, and a beacon in another workspace answers 404. MEMBER roles can call all of them except the permanent delete, which needs OWNER or ADMIN.

MethodPathWhat it does
POST/v1/beacons/{id}/promoteStart a spec from the beacon.
PATCH/v1/beacons/{id}Edit title, summary, PM notes, or hold and un-hold.
POST/v1/beaconsGroup signals into a new beacon by hand.
POST/v1/beacons/{id}/signalsAdd member signals.
DELETE/v1/beacons/{id}/signalsRemove member signals.
POST/v1/beacons/{id}/mergeFold other beacons into this one.
DELETE/v1/beacons/{id}Dismiss the beacon (soft).
DELETE/v1/beacons/{id}/permanentDelete the beacon and its spec, cards and PR links for good.
GET/v1/beacons/{id}/signalsRead every member signal, strongest first.
GET/v1/beacons/{id}/removal-previewSee what a permanent delete would remove, and the exact title to confirm.
GET/v1/beacons/{id}/timelineEvidence, milestones and rank per day for one beacon.
GET/v1/beacons/stats/timelineBeacons formed per day by slice (range 7d, 30d or 90d).
POST/v1/beacons/filter/aiTurn a sentence into a filter expression without running it.

Member lists in a request take 1 to 100 signal ids, all UUIDs. Creating, merging, promoting and permanently deleting each have their own budget of 15 requests per 30 seconds, counted per client address.

Promote a beacon to a spec

POST/v1/beacons/{id}/promote
app session only

POST /v1/beacons/{id}/promote starts spec generation from the beacon's signals and creates the spec document immediately. The body is optional: repositoryId (string, up to 64 characters) picks the repository the spec is grounded in. Without it nima uses the only indexed repository, or ranks your indexed repositories against the beacon's evidence and searches up to three of them.

The response is { beaconId, notification, specId, evidenceCount }. specId is the new spec document, which exists from the call even though its body fills in as research and writing finish; notification is the progress record for the run. Up to 20 member signals ride along, ranked. The beacon's pmNotes become the writer's brief. When generation finishes, the beacon moves to promoted and promotedSpecId is set. When it fails, the in-flight marker clears and you can promote again.

The route is declared under plan feature copilot_chat. A workspace whose plan lacks it can get 403 INSUFFICIENT_PERMISSIONS with details.feature set to copilot_chat, so handle it.

StatusWhen
400The beacon is dismissed.
400The beacon has no member signals (A beacon with no evidence cannot be promoted).
400No indexed repository yet, or Repository is not indexed or not in this organization when repositoryId is given.
409The beacon is already promoted.
409Spec generation is already in progress. A run older than 10 minutes counts as abandoned and can be promoted again.

See Anatomy of a spec for what the run produces and the 24 hour decision pause.

Hold, un-hold and edit a beacon

PATCH/v1/beacons/{id}
app session only

PATCH /v1/beacons/{id} changes any of title (up to 300 characters, not blank), summary (up to 10000), status, heldReason (up to 1000) and pmNotes (up to 10000). Send only the fields you change.

  • To hold a beacon, send status: "held" with a heldReason. Holding without a reason is a 400 reading heldReason is required to hold a beacon.
  • To un-hold, send status: "ready" or "forming", with heldReason: null to clear the old reason.
  • status: "promoted" is refused with 409: only the promote route sets it.
  • A beacon promoted to a spec that still exists cannot change status (409, delete the spec to un-promote it).

Group, add, remove and merge beacon signals

POST /v1/beacons takes { "title": "...", "signalIds": [ ... ] } with title optional and 1 to 100 ids. A signal belongs to at most one beacon, so grouping moves each signal out of any other open beacon it was in. If a signal's current beacon is promoted the call is a 409 (Signal is already evidence behind a promoted beacon), and if none of the ids are in your workspace it is a 404. Ids must be version-4 UUIDs. Both this route and the add route below return the beacon with skippedSignalIds: ids not found in your workspace are skipped, and the call fails with 404 only when none are found.

POST /v1/beacons/{id}/signals adds signals and DELETE /v1/beacons/{id}/signals removes them, both with { "signalIds": [ ... ] }. Adding to a dismissed or promoted beacon is a 409 (Beacon is no longer active). Removing every member leaves an empty beacon; it is not deleted. Both queue a fresh ask line (summary) about 15 seconds later.

POST /v1/beacons/{id}/merge takes { "sourceIds": [ ... ] }. Their evidence moves into the beacon in the path and each source is dismissed. If any source is promoted, the whole merge fails with 409. nima never merges beacons on its own: two open beacons whose evidence later overlaps stay separate until a person merges them.

Dismiss or permanently delete a beacon

DELETE /v1/beacons/{id} dismisses the beacon and keeps its history. DELETE /v1/beacons/{id}/permanent needs OWNER or ADMIN, never accepts an OAuth token, and cascades to the spec, its cards, PR links and drift checks. Its body is { "confirmTitle": "...", "signalDisposition": "release" | "dismiss" }. confirmTitle must equal the beacon title, or the call is a 400. release returns the member signals to the pool of ungrouped signals, where nima may place them in a new beacon within about a minute, and dismiss dismisses them too. It is a 409 while spec generation is running for the beacon.

How the beacon status field reaches ready

A beacon nima opens, or one with an urgent incident, is ready at once; one grouped by hand needs 3 member signals and rank 70. The full rules are in When a beacon becomes ready.

Disposition of a beacon: fix or decide

disposition on a beacon is fix or decide, set when nima opens it. fix marks a plain defect where no product choice remains, and decide is the default whenever nima is unsure. It changes nothing about which routes you can call; it only tells a person how much judgment the beacon needs.

Beacon errors and rate limits

StatuserrorWhen
400VALIDATION_ERRORUnknown slice, malformed filter, unknown facet field, or paging past 3000 rows.
401TOKEN_INVALIDMissing, unknown, revoked or expired key.
403INSUFFICIENT_PERMISSIONSThe key is missing signals:read, or the path is a session-only route. Promote can also return this when the workspace plan lacks copilot_chat.
409CONFLICTA session-only write hit a beacon in the wrong state, such as promoting one twice.
404RESOURCE_NOT_FOUNDNo such beacon in this workspace.
429RATE_LIMITEDOver the request budget.
500INTERNAL_SERVER_ERRORIncludes an id that is not a UUID.

Errors use the standard envelope. Below 500 it carries details; at 500 and above it carries an incidentId instead, which is also the X-Request-Id header on the response.

{
  "statusCode": 404,
  "error": "RESOURCE_NOT_FOUND",
  "message": "Beacon not found",
  "details": { "beaconId": "7d3c9e52-4b18-4a6f-9c20-5e81f7a0d4b3" },
  "timestamp": "2026-09-21T08:31:55.104Z",
  "path": "/v1/beacons/7d3c9e52-4b18-4a6f-9c20-5e81f7a0d4b3"
}

The three read routes carry no budget of their own, so they draw on the default: 100 requests per 60 seconds. The counter is keyed by client address, not by key, so two keys calling from one machine share one bucket. A successful response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 carries Retry-After in seconds instead, and its body is the envelope above with the message ThrottlerException: Too Many Requests and no retry field.