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
| Operation | Method | Path | Credential | MCP tool |
|---|---|---|---|---|
| Send a signal | POST | /v1/webhooks/signals/{id} | the endpoint's secret | none |
| List endpoints | GET | /v1/signal-webhooks | app session | none |
| Add an endpoint | POST | /v1/signal-webhooks | app session, owner or admin | none |
| Rotate a secret | POST | /v1/signal-webhooks/{id}/rotate | app session, owner or admin | none |
| Delete an endpoint | DELETE | /v1/signal-webhooks/{id} | app session, owner or admin | none |
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
/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.
iduuidrequired400.X-Nima-Webhook-SecretstringrequiredIdempotency-KeystringThe 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.
acceptedbooleanrequiredfalse is not an error and not a cue to retry.reasonenumrequiredduplicate, rate_limited, or null when the delivery was queued.Errors
| Status | error | When |
|---|---|---|
400 | VALIDATION_ERROR | The body is not a JSON object, or the path id is not a UUID. |
401 | TOKEN_INVALID | No 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. |
413 | VALIDATION_ERROR | The body is larger than 100 KB. |
429 | RATE_LIMITED | The 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:
| Limit | Value |
|---|---|
| Lines kept | 80 |
| Nesting depth | 6 |
| Characters per array, or per object past the depth limit | 500 (plain strings are cut only by the total) |
| Total content | 8,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:
| Pattern | Replaced 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 assignments | the label followed by [redacted] |
| Private key blocks | [redacted private key] |
| URL query strings, fragments and credentials | removed 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:
- The
Idempotency-Keyheader. - For a body that carries an
incidentobject (read undertriageif the body has atriageobject, else underreport, 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. - A top-level
id,event_idoreventIdin the body. - 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:
| Field | What nima reads |
|---|---|
error | type (or name), code, message, and frames or stack (up to 8 frames) |
kind or type | "bug" |
expectedBehavior, actualBehavior | plain text |
scope | project, service, environment |
impact | severity, affectedCount, occurrenceCount, outcome |
incident | the alert object described above |
evidenceRefs | up 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.
| Limit | Value | What happens past it |
|---|---|---|
| Per endpoint, per clock hour | the endpoint's hourlyCap, 1 to 1000, default 50 | 202 with "reason": "rate_limited" |
| Per endpoint, per minute | 50, fixed | 202 with "reason": "rate_limited" |
| Request body | 100 KB of JSON | 413 |
| Transport throttle on the ingest URL | 300 requests per 10 seconds per sending IP address | 429 |
| Endpoints per workspace | 10 | the add call answers 409 |
| Deduplication window | 24 hours | 202 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
/v1/signal-webhooksEvery 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
| Status | error | When |
|---|---|---|
401 | TOKEN_INVALID | No session, or one that expired. |
403 | INSUFFICIENT_PERMISSIONS | An API key or an OAuth access token. |
429 | RATE_LIMITED | The general API throttle, 100 requests per minute. |
Add a webhook endpoint
/v1/signal-webhooksNeeds an owner or admin session. The response is the only time the secret is shown. A workspace may hold 10 endpoints.
namestringrequiredhourlyCapinteger202.Answers 201 with a webhook endpoint object plus secret.
Errors
| Status | error | When |
|---|---|---|
400 | VALIDATION_ERROR | An empty name, one over 80 characters, or an hourlyCap outside 1 to 1000. |
401 | TOKEN_INVALID | No session, or one that expired. |
403 | INSUFFICIENT_PERMISSIONS | The session lacks the permission to manage integrations, or the caller used a key. |
409 | CONFLICT | The workspace already holds 10 endpoints. Delete one to add another. |
429 | RATE_LIMITED | The general API throttle, 100 requests per minute. |
Rotate a webhook secret
/v1/signal-webhooks/{id}/rotateNeeds 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.
iduuidrequired400.Answers 201 with the webhook endpoint object, the new secretHint, and the new secret, shown once. Counters and createdAt carry over.
Errors
| Status | error | When |
|---|---|---|
400 | VALIDATION_ERROR | The id is not a UUID. |
401 | TOKEN_INVALID | No session, or one that expired. |
403 | INSUFFICIENT_PERMISSIONS | The session lacks the permission to manage integrations, or the caller used a key. |
404 | RESOURCE_NOT_FOUND | No such endpoint in this workspace. |
429 | RATE_LIMITED | The general API throttle, 100 requests per minute. |
Delete a webhook endpoint
/v1/signal-webhooks/{id}Needs an owner or admin session. Deleting is permanent: later deliveries get 401, and signals already created from the endpoint stay.
iduuidrequired400.Answers 204 with no body.
Errors
| Status | error | When |
|---|---|---|
400 | VALIDATION_ERROR | The id is not a UUID. |
401 | TOKEN_INVALID | No session, or one that expired. |
403 | INSUFFICIENT_PERMISSIONS | The session lacks the permission to manage integrations, or the caller used a key. |
404 | RESOURCE_NOT_FOUND | No such endpoint in this workspace. |
429 | RATE_LIMITED | The 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.
iduuidrequirednamestringrequiredsecretHintstringrequiredhourlyCapintegerrequiredreceivedCountintegerrequireddroppedCountintegerrequiredlastReceivedAtISO 8601requirednull if none has. Dropped and duplicate deliveries, and incident notifications that update an existing signal, do not change it.createdAtISO 8601requiredsecretstring