Skip to main content
Niadra tells your systems when something happens in the memory, through signed webhooks. There are two kinds. A webhook subscription delivers every occurrence of an event type, such as action.recorded. A trigger is a rule of yours over what the memory knows, with parameters, a window and deduplication per subject, such as “a company promise one day past due”. In both cases Niadra notifies by webhook, and your agent or your system acts. A trigger never calls a system of record, never opens a ticket, never messages the end customer and never branches.

Outgoing event types

Endpoints, subscriptions and trigger rules are versioned configuration in the control API, changed through diffs a person approves. Every endpoint belongs to a source, and an event about the memory only reaches an endpoint whose source may read the item it is about, under the same policy as the context: purpose, audience and the verification level the source can reach. With the starter policy, the outcome of a health conversation never reaches the billing CRM, and trait.added for overdue_invoices only reaches sources with the billing purpose. A trigger rule is only accepted, and only fires, if its condition and payload are readable by the endpoint’s source: a diff that would leave a rule silent is refused when it is sent and again when it is approved. trait.present(overdue_invoices) never reaches a marketing system. Conditions and warnings count only the items that arrived through confirmed handles: an episode that came through an unconfirmed link completes no recurrence and fires no recontact, and the webhook never carries its id. A confirmation, a merge or a split re-checks the profile’s rules right away, and a split retracts the firings whose evidence left with the handle. An endpoint with format ocsf or hec does not receive these events: it receives the space’s stream of receipts for your SIEM, signed and with the same retries. See Receipts and audit. In the Console, the Alerts screen builds the rule from the catalog, with no JSON: you pick the condition, the parameters and the endpoint, test it and send it for approval with a reason. A rule in use is adjusted there too, with the same ID and the next version, and turned on or off by a diff. The control plane checks the diff when it arrives and again at approval, against the documents of that moment, and refuses:
  • a rule whose endpoint cannot receive the warning: it is a SIEM stream, does not subscribe to trigger.fired, belongs to an analysis or export source, is a customer agent receiving an identity warning, or shares no purpose or audience with the pattern of the warning;
  • a trait.present rule that names a pattern that is off or does not exist;
  • a context_use.repetition_rate rule with parameters the sweep cannot read, or with a source_id that is not in the space;
  • an endpoint that subscribes to an event type Niadra does not send: the accepted list is exactly the table above;
  • a new endpoint or mapping for a source that was cut;
  • a document with more than 50 warning rules.
Only what the diff introduces counts: a problem already stored never blocks another change. Turning an endpoint off is a choice, and its rules wait for it to come back. Cutting the source that owns endpoints, SIEM streams or a mapping always works: its keys stop at once, and those objects leave the cell’s configuration with it (pending deliveries end with endpoint_disabled, the mapping’s webhooks are refused, and the warning rules aimed at them stop). The documents keep them, as history.

The outcome of every conversation

For your CRM to record how each conversation ended, subscribe to conversation.outcome, open_item.closed and action.recorded. None of them carries the text of the conversation.
conversation.outcome goes out when the memory of a conversation or task is ready (by: extraction, with the open items it left open in open_item_ids) and when a correction changes the outcome (by: feedback, with a higher version). It carries the labels of your closed lists, such as intent, normalized category and outcome, never the summary or the resolution. conversation_id is the conversation id in your system, and it is null when it looks like a phone number, an e-mail or a WhatsApp id. open_item.closed says which open item closed (open_item_id, owner, object, expected_operation, due_at and, in opened_in, the session and the source that opened it) and what closed it, in closed_by: kind action, with action_id and operation, when an agent’s action did; system_event, with event_id, when an event of the system of record did; conversation, with session_id, when a later conversation resolved it; or feedback. links.open opens the object’s timeline or, without an object, the profile’s memory. open_item.reopened undoes a closing when the session of the closing action had a turn flagged as injection and the action was quarantined (reason: closing_action_quarantined, with the action_id).

The trigger catalog

New conditions come through the product catalog, never through free text. A rule fires at most once per subject per window (24 hours by default), and the same evidence never fires the same rule twice; the ceiling is 100 firings per minute per space, and a space holds at most 50 trigger rules, on or off. Conditions about a profile are evaluated right after its memory changes; deadlines and conditions about a source, every 5 minutes. A late firing, more than 2 hours after the condition became true (or what late_after_minutes says), is marked late or suppressed, as the rule says.

Conditions about a source

source.silent, source.gap and context_use.repetition_rate are about a source, not a customer. The subject of the notice is the source ("kind": "source", with its id in pseudonym), and the 5-minute sweep evaluates all three.
  • source.silent watches the sources that sent events in the last 30 days. Each silence is its own occurrence: a source that comes back and falls silent again warns again.
  • context_use.repetition_rate reads the daily measurement of Context use. It fires when, over the last window_days days (7 by default, up to 90), repeated questions exceed the above share (0.2 by default) of the source’s questions, with at least min_questions questions (30 by default) for the number to count. Without source_id, each source is watched on its own. The control group of an experiment is left out, since it never received the context. A period above the threshold warns once, counted from the day it began; if the rate drops and rises again, a new notice goes out. The notice carries in data.detail the questions (questions), the repeated ones (repeated), the rate (rate), the threshold (above), the window (window_days) and the day the period began (above_since), and links.open points to /v1/context-use?source_id=.... Parameters the sweep cannot read, or a source_id that does not exist in the space, are refused when the diff is sent and when it is approved.
Before turning a rule on, run it dry with POST /v1/triggers/dry-run: the draft names rule_id, condition, params, the target endpoint_id, window_hours (24 by default, up to 90 days) and late_policy (deliver_marked or suppress). The answer says for how many of the customers active in the last 30 days (subjects_evaluated) the condition holds now (would_fire), with a sample of up to 10, and target_problem when the endpoint’s source may not read the payload. For source.silent and context_use.repetition_rate, the dry run evaluates the space’s sources as the sweep would see them now. It checks the condition; the window and the deduplication only apply to real firings, and nothing fires. Adjust the parameters before anyone gets paged. It takes the integration role or an admin key.
Example: “notify the CRM one day after a missed technician visit”. When Marina’s rescheduled visit passes its due date by one day with no action that closes it, Niadra sends trigger.fired to the CRM endpoint, and your CRM agent reschedules the visit.

What a delivery looks like

Deliveries follow the open Standard Webhooks format.
The body carries ids, the rule and its version, state and pointers to evidence. It never carries conversation content. The subject is the customer’s pseudonym and kind, never a phone, an e-mail or a document number. links.open is the API route that opens what the notice points at: the profile, the memory, an object’s timeline, an episode or a source’s measurement. It only works with a credential of your space that has the scope for it. Three headers come with every delivery:
  • webhook-id: unique per message; the same on every retry. Use it to deduplicate.
  • webhook-timestamp: Unix seconds. Reject anything more than 5 minutes away from your clock.
  • webhook-signature: v1, followed by the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}. During a secret rotation, two signatures separated by a space are valid at once.

Verify the signature

The secret is written once, through the Console or PUT /v1/secrets/webhook/{id}, and starts with whsec_; the key is the base64 part after the prefix. Verify on the raw body, before parsing it.

Delivery guarantees

  • At least once. A message may arrive more than once; deduplicate by webhook-id.
  • No ordering. Messages may arrive out of order; every event carries as_of and a version so you can order them.
  • Retries with exponential backoff, from 30 seconds up to 4 hours between them, for up to 24 hours. After that the delivery goes to the dead-letter queue, visible in the Console, and is deleted 7 days after it was created.
  • An endpoint failing for 24 hours straight, without a single success, stops receiving: new deliveries to it go straight to the dead-letter queue with the error endpoint_failing_24h. A manual resend that succeeds opens it again.
  • Controlled egress. Every outgoing call goes out over HTTPS only and never follows a redirect. The address is resolved and checked on each attempt, and the connection goes to the checked address. Private, loopback, link-local (the cloud metadata address included), CGNAT, reserved and multicast addresses are refused.

The notifications feed

Those who prefer to pull instead of receiving read the same events through GET /v1/notifications: the webhook bodies, oldest first, by cursor, ids and pointers only. Each item has the fields of a delivery (id, type, created_at, tenant, subject, data and links); the id is the event’s, and a webhook delivery of the same event has an id of its own, so a reader of both deduplicates by the event id. Without a cursor, the feed starts from the beginning; next_cursor is where the next poll starts, and a poll with nothing new answers the same cursor. Events stay 7 days. An event aimed at one endpoint, or about a memory item only some sources may read, reaches its webhooks only, so the feed carries what any source of the space may see, and never a conversation’s text. Any source key reads the feed, whatever its scopes. The feed is the space’s notifications feature, off by default and turned on in the features document by an approved diff of the security role; in a space without it, the route answers 404.

Answering a delivery

Answer with any 2xx quickly and do the work asynchronously. A delivery is pending, delivered, failing while it retries, or dead. These routes take a Console person with the integration role (firings also analysis) or a key with the admin scope. To inspect deliveries use Webhook deliveries; to send a dead delivery again, with the same webhook-id, use Redeliver. To check an endpoint or a SIEM stream before you rely on it, Send a test delivery sends once, signed like the rest, a delivery of type webhook.test: the usual envelope with data: {"test": true}, or an OCSF event marked as a test for the SIEM. It shows in the deliveries list. Firings, with the rule, the version and the evidence, are listed at Trigger firings.

Next steps

Patterns

the signals trait.present can watch.

Webhooks from your systems

the other direction, events coming in.

Context use

the repetition rate behind context_use.repetition_rate.

Receipts and audit

receipts delivered to your SIEM through the same mechanism.