Skip to main content

Webhooks

Receive real-time notifications when risk thresholds are exceeded.

Overview

Webhooks allow you to receive HTTP POST requests when evaluations exceed configured thresholds. This enables real-time alerting, logging, and escalation workflows without polling.

Event Types

EventSourceDescription
evaluate.alert/v1/evaluateUser risk severity meets or exceeds your configured threshold
oversight.alert/v1/oversight/*AI behavior concern level is high or critical
oversight.ingestion.complete/v1/oversight/ingestBatch ingestion processing has completed
test.pingDashboard/APITest event to verify your endpoint

Setting Up Webhooks

Configure webhooks in the dashboard or via the API. You'll need:

  • Endpoint URL — HTTPS URL to receive POST requests (localhost allowed for testing)
  • Severity threshold — minimum severity to trigger: none, low, medium, high, or critical
  • Include conversation: product-specific context described below

Your Signing Secret

When you create a webhook, NOPE generates a unique signing secret for that endpoint. This secret is used to verify that webhook requests genuinely came from NOPE.

Important: Save Your Secret

  • The secret is only shown once when you create the webhook
  • Store it securely in your environment variables (e.g., NOPE_WEBHOOK_SECRET)
  • If you lose it, use "Regenerate Secret" in the dashboard — but update your endpoint immediately, as old signatures will no longer validate

The secret looks like: whsec_a1b2c3d4e5f6...

API Routes

Manage webhooks programmatically:

MethodEndpointDescription
POST/v1/webhooksCreate a webhook
GET/v1/webhooksList all webhooks
GET/v1/webhooks/:idGet a webhook
PUT/v1/webhooks/:idUpdate a webhook
DELETE/v1/webhooks/:idDelete a webhook
POST/v1/webhooks/:id/regenerate-secretRegenerate signing secret
POST/v1/webhooks/:id/testSend a test ping
GET/v1/webhooks/:id/eventsList recent events

Webhook Payloads

When a threshold is exceeded, NOPE sends a JSON payload. All payloads share a common structure:

{
  // Webhook bodies are flat — there is no "data" envelope.
  // These four fields appear on every event:
  "event": "evaluate.alert" | "oversight.alert" | "oversight.ingestion.complete" | "test.ping",
  "event_id": "evt_abc123def456",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "api_version": "2025-01"
  // ...followed by the event-specific fields shown below.
  //
  // The webhook config ID is sent in the X-NOPE-Webhook-ID request header,
  // alongside X-NOPE-Signature and X-NOPE-Timestamp — not in the body.
}

evaluate.alert Payload

Sent when a user evaluation meets your configured risk threshold. Includes the full risk assessment to enable immediate triage:

{
  "event": "evaluate.alert",
  "event_id": "evt_abc123def456",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "api_version": "2025-01",
  "conversation_id": "conv_67890",   // Your ID (if provided)
  "user_id": "user_12345",           // Your user ID (if provided)

  "risk_summary": {
    "overall_severity": "high",
    "overall_imminence": "urgent",
    "primary_domain": "suicide",
    "confidence": 0.88,
    "primary_concerns": "User expressing active suicidal ideation with specific plan"
  },

  "domains": [
    { "domain": "suicide", "severity": "high", "imminence": "urgent" }
  ],

  "flags": {
    "intimate_partner_violence": null,
    "child_safeguarding": null,
    "third_party_threat": false
  },

  "resources_provided": [
    { "name": "988 Suicide & Crisis Lifeline", "type": "crisis_line", "country": "US" }
  ],

  "conversation": {
    "included": true,           // Only if include_conversation enabled
    "message_count": 5,
    "latest_user_message": "I can't do this anymore...",
    "truncated": false
  }
}

Use this to:

  • Alert human moderators for high-risk conversations
  • Log incidents for safety team review
  • Trigger automated follow-up workflows

oversight.alert Payload

Sent when AI behavior analysis detects concerning patterns in agent responses:

{
  "event": "oversight.alert",
  "event_id": "evt_abc123def456",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "api_version": "2025-01",
  "conversation_id": "conv_67890",
  "concern": "high",
  "trajectory": "worsening",
  "summary": "The AI encouraged social isolation and positioned itself as the user's sole support.",
  "behaviors": [
    {
      "code": "dependency_reinforcement",
      "name": "Dependency Reinforcement",
      "severity": "high",
      "category": "boundary_violations"
    }
  ],
  "agent_ids": ["agent_companion_v2"],
  "platform": "companion_app",
  "user_is_minor": false,
  "conversation": {
    "included": true,
    "message_count": 18
  }
}

Use this to:

  • Flag AI responses for safety review
  • Identify systematic issues with AI behavior
  • Trigger agent retraining or prompt updates

oversight.ingestion.complete Payload

Sent when batch conversation processing completes:

{
  "event": "oversight.ingestion.complete",
  "event_id": "evt_abc123def456",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "api_version": "2025-01",
  "ingestion_id": "ing_xyz789",
  "conversations_total": 1250,
  "conversations_processed": 1245,
  "conversations_failed": 5,
  "concerns": { "none": 1100, "low": 90, "medium": 32, "high": 18, "critical": 5 },
  "top_behaviors": [
    { "code": "sycophantic_validation", "name": "Sycophantic Validation", "occurrence_count": 210 }
  ],
  "processing_time_ms": 45230
}

test.ping Payload

Sent when testing webhook configuration via the dashboard or API:

{
  "event": "test.ping",
  "event_id": "evt_abc123def456",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "api_version": "2025-01",
  "message": "Webhook configured successfully"
}

HTTP Headers

Every webhook request includes these headers:

HeaderDescription
X-NOPE-SignatureHMAC-SHA256 signature: sha256=<hex>
X-NOPE-TimestampUnix timestamp (seconds) when sent
X-NOPE-EventEvent type (evaluate.alert, oversight.alert, etc.)
X-NOPE-Delivery-IDUnique delivery ID for debugging
X-NOPE-Webhook-IDYour webhook configuration ID
Content-Typeapplication/json
User-AgentNOPE-Webhooks/1.0

Verifying Signatures

Always verify webhook signatures to ensure requests came from NOPE and weren't tampered with.

The signature is computed as HMAC-SHA256(secret, timestamp + "." + payload). Verify the exact raw request body bytes before any JSON parser runs. Parsing and re-serializing JSON can change the signed bytes, including for non-ASCII content.

import { Webhook, WebhookSignatureError } from '@nope-net/sdk';

// Web-standard handler. Read bytes before calling request.json().
async function handleNopeWebhook(request: Request): Promise<Response> {
  const rawBody = new Uint8Array(await request.arrayBuffer());

  try {
    const verified = Webhook.verifyRequest(
      rawBody,
      request.headers,
      process.env.NOPE_WEBHOOK_SECRET!
    );

    console.log(verified.deliveryId, verified.payload.event);
    return new Response('OK', { status: 200 });
  } catch (error) {
    if (error instanceof WebhookSignatureError) {
      return new Response('Invalid signature', { status: 401 });
    }
    throw error;
  }
}

Security Notes

  • Always verify the signature before processing
  • Check timestamp freshness to prevent replay attacks (recommended: 5 minute window)
  • Use constant-time comparison to prevent timing attacks
  • Store your webhook secret securely (treat like an API key)
  • Deduplicate retries using X-NOPE-Delivery-ID

Including Context

To correlate webhook events with your data, include conversation_id and end_user_id in evaluate requests:

const result = await nope.evaluate({
  messages: [...],
  config: {
    country: "US",
    conversation_id: "conv_abc123",  // Your conversation ID
    end_user_id: "user_xyz789"       // Your user ID
  }
});

These IDs are included in webhook payloads, allowing you to look up the conversation and user in your system.

Responding to Webhooks

Return a 2xx status code within 30 seconds to acknowledge receipt. NOPE retries network failures, 408, 429, and 5xx responses on this schedule:

  • Attempt 1: Immediate
  • Attempt 2: 1 minute
  • Attempt 3: 10 minutes
  • Attempt 4: 1 hour

After 4 failed attempts, the event is marked as failed. View delivery history in the dashboard.

Conversation Content

The include_conversation option is product-specific. Evaluate alerts can include the latest user message:

"conversation": {
  "included": true,
  "message_count": 5,
  "latest_user_message": "I can't do this anymore...",
  "truncated": false  // true if message was >1000 chars
}

Oversight alerts include only { included: true, message_count } in their conversation object. They do not include the transcript or latest message. Ingestion-complete and test-ping events have no conversation content.

Privacy Note

Message content is only included if you explicitly enable it. By default, webhooks contain only risk metadata without conversation content.

Example: Slack Alert

app.post('/webhooks/nope', async (req, res) => {
  const { payload } = Webhook.verifyRequest(
    req.body,
    req.headers,
    process.env.NOPE_WEBHOOK_SECRET
  );

  if (payload.event !== 'evaluate.alert') return res.status(200).send('OK');
  const { event, risk_summary } = payload;

  if (risk_summary.overall_severity === 'high' ||
      risk_summary.overall_severity === 'critical') {
    await slack.send({
      text: `:warning: Risk Alert: ${event}`,
      blocks: [{
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Severity:* ${risk_summary.overall_severity}
*Imminence:* ${risk_summary.overall_imminence}
*Primary Domain:* ${risk_summary.primary_domain}
*Concerns:* ${risk_summary.primary_concerns}`
        }
      }]
    });
  }

  res.status(200).send('OK');
});

Testing Webhooks

Use the dashboard or API to send test pings:

curl -X POST https://api.nope.net/v1/webhooks/whk_xxx/test \
  -H "Authorization: Bearer YOUR_API_KEY"

This sends a test.ping event to verify your endpoint is reachable and signature verification works.

Next Steps