Skip to main content 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: low, medium, high, or critical
  • Include conversation — optionally include the latest message content

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:

{
  "event": "evaluate.alert" | "oversight.alert" | "oversight.ingestion.complete" | "test.ping",
  "webhook_id": "wh_xxx",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": { /* event-specific payload */ }
}

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",
  "webhook_id": "wh_abc123def456",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "request_id": "req_xyz789",
    "end_user_id": "user_12345",
    "conversation_id": "conv_67890",
    "summary": {
      "speaker_severity": "high",
      "speaker_imminence": "urgent",
      "any_third_party_risk": false,
      "primary_concerns": "User expressing active suicidal ideation with specific plan"
    },
    "risks": [
      {
        "subject": "self",
        "subject_confidence": 0.92,
        "type": "suicide",
        "severity": "high",
        "imminence": "urgent",
        "confidence": 0.88,
        "features": ["active_ideation", "plan_present", "hopelessness"]
      }
    ],
    "legal_flags": {
      "ipv": null,
      "safeguarding_concern": null,
      "third_party_threat": null
    },
    "confidence": 0.88,
    "crisis_resources": [
      {
        "type": "crisis_line",
        "name": "988 Suicide & Crisis Lifeline",
        "phone": "988",
        "is_24_7": true
      }
    ]
  }
}

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",
  "webhook_id": "wh_abc123def456",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "analysis_id": "oa_xyz789",
    "end_user_id": "user_12345",
    "agent_id": "agent_companion_v2",
    "conversation_summary": {
      "concern_level": "high",
      "trajectory": "worsening",
      "behaviors_detected": [
        {
          "behavior_id": "romantic_escalation_with_minor",
          "severity": "high",
          "confidence": 0.91,
          "message_indices": [12, 15, 18]
        }
      ]
    },
    "recommendation": "immediate_human_review"
  }
}

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",
  "webhook_id": "wh_abc123def456",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "batch_id": "batch_xyz789",
    "conversations_processed": 1250,
    "conversations_flagged": 23,
    "high_concern_count": 5,
    "processing_time_ms": 45230,
    "status": "completed"
  }
}

test.ping Payload

Sent when testing webhook configuration via the dashboard or API:

{
  "event": "test.ping",
  "webhook_id": "wh_abc123def456",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "message": "Webhook endpoint is configured correctly"
  }
}

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)

const crypto = require('crypto');

function verifyWebhookSignature(req, secret) {
  const payload = JSON.stringify(req.body);
  const signature = req.headers['x-nope-signature'];
  const timestamp = req.headers['x-nope-timestamp'];

  // Check timestamp freshness (prevent replay attacks)
  const now = Math.floor(Date.now() / 1000);
  const age = now - parseInt(timestamp, 10);
  if (Math.abs(age) > 300) { // 5 minutes
    throw new Error('Timestamp too old or in future');
  }

  // Compute expected signature
  const message = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');

  // Verify signature (constant-time comparison)
  const received = signature.replace('sha256=', '');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(received)
  );
}

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)

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: {
    user_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 to acknowledge receipt. NOPE will retry failed deliveries with exponential backoff:

  • 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

If you enable include_conversation, webhooks 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
}

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) => {
  // Verify signature first
  if (!verifyWebhookSignature(req, process.env.NOPE_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const { event, risk_summary, domains } = req.body;

  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