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
| Property | Both modes |
|---|---|
| Delivery | at-least-once |
| Ordering | monotonic by sequence per patientId |
| Deduplication | the consumer; key on eventId |
| Backpressure | the consumer; the server does not throttle event production |
| Replay | by 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.
| Event | Payload includes |
|---|---|
reflection.submitted | reflectionId, wordCount, redactionStatus |
scale.captured | instrument, instrumentVersion, score, severity |
risk.flagged | item9Value, safetySurfaceState, clinicianNotifiedAt |
draft.promoted | draftId, clinicianId, committedAt |
audit.appended | auditId, 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.