docs
API

Webhooks

Send signals to nima from any tool that can POST JSON: creating an endpoint, the ingest call, dedupe, limits, redaction and structured bug reports.

A signal webhook is an endpoint you create that anything able to POST JSON can send to: a support tool, an alerting channel, a cron job, an internal admin tool. Each endpoint has its own secret and its own budget. Each accepted delivery becomes a signal (or updates one, for an incident), the first stage of the loop.

On for every workspace. Sending to an endpoint needs only its secret, no API key. Creating, rotating and deleting endpoints needs a signed-in owner or admin session, and the admin panel has no control for it today: call POST /v1/signal-webhooks with the session, or ask the nima team.

Webhook operations

OperationMethodPathCredentialMCP tool
Send a signalPOST/v1/webhooks/signals/{id}the endpoint's secretnone
List endpointsGET/v1/signal-webhooksapp sessionnone
Add an endpointPOST/v1/signal-webhooksapp session, owner or adminnone
Rotate a secretPOST/v1/signal-webhooks/{id}/rotateapp session, owner or adminnone
Delete an endpointDELETE/v1/signal-webhooks/{id}app session, owner or adminnone

Paths are relative to https://api.getnima.io. No operation on this page carries an OAuth scope, and there is no MCP tool for webhooks. See authentication for the other credentials.

Creating a webhook endpoint

A webhook endpoint is created with POST /v1/signal-webhooks from a signed-in owner or admin session. There is no admin-panel control for it: use the call below with the app's session token, or ask the nima team to create one. A workspace holds at most 10 endpoints.

The response carries a URL id and a secret that starts with nwh_. The secret is shown once, at creation and at rotation. nima stores only a hash, so a lost secret can only be replaced by rotating. The endpoint URL is:

https://api.getnima.io/v1/webhooks/signals/<id>

An API key or an OAuth access token cannot manage endpoints: those routes declare no scope, so such a token gets 403. The add call has the details.

Send a signal to a webhook endpoint

POST/v1/webhooks/signals/{id}

The only credential is the endpoint's secret. Send it in the X-Nima-Webhook-Secret header, or as the Basic auth password if your sender cannot set headers. Authorization: Bearer does not work.

A valid secret always answers 202, including when the delivery is discarded. Read accepted in the body, not the status code.

iduuidrequired
The endpoint's id, the last segment of its URL. A value that is not a UUID answers 400.
X-Nima-Webhook-Secretstringrequired
The endpoint's secret. The Basic auth password is the alternative, not an addition.
Idempotency-Keystring
Wins over every other deduplication rule. Two deliveries with the same key inside 24 hours produce one signal. A key over 200 characters is hashed first.

The body is any JSON object; nothing in it is required. Send it with Content-Type: application/json. An array or a bare string answers 400.

acceptedbooleanrequired
Response field. Whether the delivery became a signal. false is not an error and not a cue to retry.
reasonenumrequired
Response field. duplicate, rate_limited, or null when the delivery was queued.

Errors

StatuserrorWhen
400VALIDATION_ERRORThe body is not a JSON object, or the path id is not a UUID.
401TOKEN_INVALIDNo secret, a wrong secret, or no such endpoint. A wrong secret and an unknown id give the same answer on purpose, so an id cannot be probed without its secret.
413VALIDATION_ERRORThe body is larger than 100 KB.
429RATE_LIMITEDThe transport throttle on the ingest URL, not your hourly cap.

Send a webhook secret as a Basic auth password

Send the secret as the Basic auth password when the sender cannot set headers. The username is ignored.

curl -X POST https://api.getnima.io/v1/webhooks/signals/6f1c0b84-53a7-4e29-8d15-2c7a9e0b43f6 \
  -u "nima:$NIMA_WEBHOOK_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"title":"Bulk export times out on large workspaces"}'

Authorization: Bearer <secret> does not work. nima reads every bearer token without a dot as an OAuth access token and rejects it before the webhook route runs. Use the header or Basic auth.

What nima does with webhook JSON

nima flattens the JSON body into key: value lines, redacts them, and runs the result through the same classify, extract and embed pass as a Slack thread. The raw body is not stored. The signal is created with source webhook, and its origin is internal.

Nested objects become dotted keys, so {"customer":{"name":"Northwind"}} reads as customer.name: Northwind. Flattening stops at these limits, and anything past them is dropped, so put what matters near the top of the object:

LimitValue
Lines kept80
Nesting depth6
Characters per array, or per object past the depth limit500 (plain strings are cut only by the total)
Total content8,000 characters

For a body without a top-level incident object, the headline of the signal is the first of title, summary, message, description, text, name and subject that is present. For an incident body, the headline is the policy name plus the summary. A top-level url or link that starts with http is kept as the signal's link, with query string, fragment and credentials stripped.

Redaction of webhook content

nima redacts webhook content before it is stored. nima drops any field named authorization, password, secret, token, access_token, refresh_token, api_key (also apikey, api-key) or cookie, at any level and in any letter case; inside arrays the value shows as [redacted]. The remaining text has these patterns replaced:

PatternReplaced with
Email addresses[redacted email]
Phone numbers[redacted phone]
Bearer and Basic tokens, and authorization, password, secret, client_secret, private_key, token, access_token, refresh_token, api_key, cookie assignmentsthe label followed by [redacted]
Private key blocks[redacted private key]
URL query strings, fragments and credentialsremoved from the URL (a URL that cannot be parsed becomes [redacted URL])

Redaction is pattern matching, not a guarantee. Do not send secrets you do not want nima to hold.

Webhook weight in beacon ranking

A webhook signal carries a producer weight of 0.8 when a beacon is ranked, the same as Sentry and PostHog, on the reasoning that an inbound webhook is usually your own alerting rather than one person's opinion. See How nima decides.

Webhook deduplication

nima deduplicates webhook deliveries for 24 hours, using the first of these keys that applies:

  1. The Idempotency-Key header.
  2. For a body that carries an incident object (read under triage if the body has a triage object, else under report, else at the top level): the incident id, the incident state, and the provider timestamp when there is one. An incident that opens and later closes gives two different keys.
  3. A top-level id, event_id or eventId in the body.
  4. A SHA-256 hash of the raw body.

A repeat inside the window answers { "accepted": false, "reason": "duplicate" }. The 24-hour window only decides the duplicate receipt. A repeat of the same key after that answers accepted: true, but it lands on the signal the key already created, so it never makes a second one. Deduplication is per endpoint: two endpoints sending the same id create two signals.

Send Idempotency-Key when you can: a retry of a delivery that failed in transit is then safe, and a body whose text changes slightly between retries does not create a second signal.

Incident alerts as one signal

An incident-shaped webhook body updates a single signal instead of creating one per notification. When the body has an incident object with an id, the signal is keyed on the endpoint plus a hash of the provider and incident id. The open and the later recovery notification both land on that one signal; a notification whose provider timestamp is older than the one nima already holds is ignored.

A body with a top-level incident object is also rendered as a Google Cloud Monitoring alert. nima renders it as a report (policy, condition, resource, project, observed and threshold values, documentation, start and end) instead of flat lines, and a closed incident is previewed as recovered.

Structured bug reports over webhooks

A webhook body can carry a structured bug report, either at the top level or nested under triage or report. nima reads these fields as evidence, marks it as reported and not verified, and lists what is missing:

FieldWhat nima reads
errortype (or name), code, message, and frames or stack (up to 8 frames)
kind or type"bug"
expectedBehavior, actualBehaviorplain text
scopeproject, service, environment
impactseverity, affectedCount, occurrenceCount, outcome
incidentthe alert object described above
evidenceRefsup to 9 entries, each { kind, url, reference } with kind incident, log, trace or report

Fingerprint of a webhook bug report

A bug report gets a stable fingerprint only when service, environment, message, an error type or code, and at least one application stack frame are all present. Nothing in that identity may be redacted or truncated. Line and column numbers are ignored when comparing frames. If any part is missing the report still becomes a signal, without a fingerprint.

Bug reports and beacons

A signal with a fingerprint joins the open beacon (forming, ready or held, or promoted to a spec that has not shipped) that has the same fingerprint and scope, without asking the placement judge. A dismissed beacon or one whose spec has shipped is never reopened: with no open match, the signal forms a new beacon marked as a fix rather than a product decision. See How nima decides for grouping in general.

A report is urgent when all of these hold: the severity (impact.severity, else the report's or incident's severity) is critical, high or fatal; the outcome (impact.outcome, else outcome) is failed, terminal_failure, retry_exhausted, data_loss or unavailable; the report has an error; and the incident is not marked recovered. An urgent report makes its beacon ready at once, without waiting for more evidence. Occurrence counts do not count towards urgency.

Webhook limits

Webhook limits are per endpoint, plus one per workspace and one on the transport.

LimitValueWhat happens past it
Per endpoint, per clock hourthe endpoint's hourlyCap, 1 to 1000, default 50202 with "reason": "rate_limited"
Per endpoint, per minute50, fixed202 with "reason": "rate_limited"
Request body100 KB of JSON413
Transport throttle on the ingest URL300 requests per 10 seconds per sending IP address429
Endpoints per workspace10the add call answers 409
Deduplication window24 hours202 with "reason": "duplicate"

A delivery dropped for being over budget adds to the endpoint's droppedCount; a duplicate does not. The count is visible only over the API (the list, add and rotate responses); the admin panel does not show it. Accepted deliveries are queued for processing with 3 attempts and exponential backoff.

Managing webhook endpoints over the API

The four management operations take a signed-in nima session and never a key. An API key or an OAuth access token answers 403. They carry no stability promise, and in the examples $NIMA_SESSION stands for the app's session token.

List webhook endpoints

GET/v1/signal-webhooks
app session only

Every member of the workspace can list endpoints, oldest first. Secrets never appear; secretHint is the last four characters. Each row is a webhook endpoint object.

Errors

StatuserrorWhen
401TOKEN_INVALIDNo session, or one that expired.
403INSUFFICIENT_PERMISSIONSAn API key or an OAuth access token.
429RATE_LIMITEDThe general API throttle, 100 requests per minute.

Add a webhook endpoint

POST/v1/signal-webhooks
app session only

Needs an owner or admin session. The response is the only time the secret is shown. A workspace may hold 10 endpoints.

namestringrequired
What the sender is, for telling endpoints apart. 1 to 80 characters.
hourlyCapinteger
Deliveries accepted per clock hour (a fixed window that resets on the hour). 1 to 1000, default 50. Anything over it is dropped with a 202.

Answers 201 with a webhook endpoint object plus secret.

Errors

StatuserrorWhen
400VALIDATION_ERRORAn empty name, one over 80 characters, or an hourlyCap outside 1 to 1000.
401TOKEN_INVALIDNo session, or one that expired.
403INSUFFICIENT_PERMISSIONSThe session lacks the permission to manage integrations, or the caller used a key.
409CONFLICTThe workspace already holds 10 endpoints. Delete one to add another.
429RATE_LIMITEDThe general API throttle, 100 requests per minute.

Rotate a webhook secret

POST/v1/signal-webhooks/{id}/rotate
app session only

Needs an owner or admin session. Rotating issues a new secret and invalidates the old one immediately, with no grace window: a sender still using the old secret gets 401 on its next delivery. Put the new secret into the sender as soon as the call returns; deliveries sent with the old secret in between get 401 and are not queued. The URL does not change.

iduuidrequired
The endpoint's id. A value that is not a UUID answers 400.

Answers 201 with the webhook endpoint object, the new secretHint, and the new secret, shown once. Counters and createdAt carry over.

Errors

StatuserrorWhen
400VALIDATION_ERRORThe id is not a UUID.
401TOKEN_INVALIDNo session, or one that expired.
403INSUFFICIENT_PERMISSIONSThe session lacks the permission to manage integrations, or the caller used a key.
404RESOURCE_NOT_FOUNDNo such endpoint in this workspace.
429RATE_LIMITEDThe general API throttle, 100 requests per minute.

Delete a webhook endpoint

DELETE/v1/signal-webhooks/{id}
app session only

Needs an owner or admin session. Deleting is permanent: later deliveries get 401, and signals already created from the endpoint stay.

iduuidrequired
The endpoint's id. A value that is not a UUID answers 400.

Answers 204 with no body.

Errors

StatuserrorWhen
400VALIDATION_ERRORThe id is not a UUID.
401TOKEN_INVALIDNo session, or one that expired.
403INSUFFICIENT_PERMISSIONSThe session lacks the permission to manage integrations, or the caller used a key.
404RESOURCE_NOT_FOUNDNo such endpoint in this workspace.
429RATE_LIMITEDThe general API throttle, 100 requests per minute.

The webhook endpoint object

A webhook endpoint object is one row per sender, each with its own secret and budget.

iduuidrequired
The id in the ingest URL.
namestringrequired
What you called the sender. 1 to 80 characters.
secretHintstringrequired
The last four characters of the secret.
hourlyCapintegerrequired
Deliveries accepted per clock hour (a fixed window that resets on the hour). 1 to 1000, default 50.
receivedCountintegerrequired
Deliveries that became a signal.
droppedCountintegerrequired
Deliveries dropped for being over the per-minute or hourly budget. Duplicates are not counted.
lastReceivedAtISO 8601required
When a delivery last became a new signal, or null if none has. Dropped and duplicate deliveries, and incident notifications that update an existing signal, do not change it.
createdAtISO 8601required
When the endpoint was created.
secretstring
The full secret. Present only on the add and rotate responses, never afterwards.