Skip to content
Book a demoSign in
All docs
Agent SDK

Webhooks and realtime events

Event taxonomy, two delivery modes (HTTPS webhook and Postgres-changes subscription), signed payloads, and ordering guarantees.

Updated May 29, 2026

Agents that react to patient activity (a new reflection, a captured scale, an item-9 flag) subscribe to events instead of polling. The Nyra eventing surface ships in two modes. Hosted integrations receive HMAC-signed HTTPS webhooks. Self-hosted integrations attach a Postgres-changes subscription directly to the database. The same five event types flow through both.

The event taxonomy

reflection.submitted    a patient finished a daily reflection; transcript may still be redacting
scale.captured          a PHQ-9 or GAD-7 administration completed; bands resolved
risk.flagged            item-9 escalation fired; carries the safety surface state
draft.promoted          a clinician committed an agent draft to the live record
audit.appended          any new audit entry; coarse signal for chain-watching agents

These five are the surface area. There are no per-field events (no reflection.fieldChanged); the event grain is the action a clinician would describe in a sentence.

Event envelope

Every event lands on the consumer in the same envelope:

type EventEnvelope<T> = {
  eventId: string;       // ULID; unique across modes and replays
  type: string;          // one of the five above
  clinicId: string;
  patientId: string;
  emittedAt: string;     // ISO 8601, server clock
  sequence: number;      // monotonic per-patient
  payload: T;
};

sequence is the ordering primitive. Within a single patientId, sequence numbers are dense and increasing. Across patients there is no global ordering. Across clinics there is no global ordering. Two agents reading the same patient stream will see the same sequence series.

Mode 1: HTTPS webhook

Hosted integrations register a single webhook URL per agent identity. Nyra POSTs the envelope as JSON, signed with HMAC-SHA256 over the raw body and the X-Nyra-Timestamp header.

import { verifyWebhookSignature } from "@humyn/nyra";

export async function handler(req: Request): Promise<Response> {
  const body = await req.text();
  const ok = verifyWebhookSignature({
    body,
    signature: req.headers.get("x-nyra-signature") ?? "",
    timestamp: req.headers.get("x-nyra-timestamp") ?? "",
    secret: process.env.OPENMIND_WEBHOOK_SECRET!,
    toleranceSeconds: 300,
  });
  if (!ok) return new Response("bad signature", { status: 401 });

  const envelope = JSON.parse(body);
  await onEvent(envelope);
  return new Response("ok");
}
from humyn_nyra import verify_webhook_signature

async def handler(request):
    body = await request.body()
    ok = verify_webhook_signature(
        body=body,
        signature=request.headers.get("x-nyra-signature", ""),
        timestamp=request.headers.get("x-nyra-timestamp", ""),
        secret=os.environ["OPENMIND_WEBHOOK_SECRET"],
        tolerance_seconds=300,
    )
    if not ok:
        return Response("bad signature", status=401)
    envelope = json.loads(body)
    await on_event(envelope)
    return Response("ok")

Retry policy: failed deliveries (any non-2xx within 10 seconds) retry on exponential backoff at 1s, 5s, 30s, 5m, 1h, 6h. After 6h the event is parked in the failed-delivery queue and surfaces in the operator console. The audit log retains the original emission regardless of delivery state.

Mode 2: Postgres-changes subscription

Self-hosted deployments can subscribe directly to the realtime stream the architecture doc describes. The SDK ships a thin wrapper that translates Postgres INSERT rows on the events table into the same envelope.

import { subscribeToEvents } from "@humyn/nyra";

const sub = subscribeToEvents(client, {
  patientId,
  types: ["reflection.submitted", "risk.flagged"],
  onEvent: async (envelope) => { await onEvent(envelope); },
});

// Later:
await sub.unsubscribe();
from humyn_nyra import subscribe_to_events

sub = subscribe_to_events(
    client,
    patient_id=patient_id,
    types=["reflection.submitted", "risk.flagged"],
    on_event=on_event,
)
await sub.unsubscribe()

The subscription resumes from a caller-supplied sinceSequence if you provide one. The default is "now"; events that landed before the subscription opened are not replayed unless requested.

Delivery guarantees

PropertyBoth modes
Deliveryat-least-once
Orderingmonotonic by sequence per patientId
Deduplicationthe consumer; key on eventId
Backpressurethe consumer; the server does not throttle event production
Replayby sinceSequence (subscription mode) or by audit-id re-fetch

At-least-once is load-bearing. Build the handler idempotent. The SDK does not deduplicate for you, because the durable store of "already handled" lives in your system, not Nyra's.

What is and is not in the payload

The payload carries enough state to drive the next agent action without a follow-up read in most cases. It does not carry the full evidence map or the full reflection transcript. Those are intentionally a separate read so the event stream stays cheap to fan out.

EventPayload includes
reflection.submittedreflectionId, wordCount, redactionStatus
scale.capturedinstrument, instrumentVersion, score, severity
risk.flaggeditem9Value, safetySurfaceState, clinicianNotifiedAt
draft.promoteddraftId, clinicianId, committedAt
audit.appendedauditId, action, actorRole

risk.flagged is the one event most agents handle synchronously. The full safety surface state lands in the payload so the agent does not need a follow-up read on the critical path.

Next: Data export and deletion.