Skip to main content

Overview

Webhooks allow Preclinical to notify your systems when test runs complete. Instead of polling the API, you receive an HTTP POST request with run results as soon as they’re available. Use cases for webhooks:
  • Trigger deployment pipelines based on test results
  • Post results to Slack or other communication tools
  • Update dashboards and monitoring systems
  • Store historical results in your own database

Configuration

Webhooks can be configured at two levels:
LevelDescription
OrganizationDefault webhook for all agents in your organization
AgentOverride the org webhook for a specific agent

Organization Webhook

  1. Go to Settings in the sidebar
  2. Click the Webhooks tab
  3. Enter your webhook URL
  4. Optionally enter a signing secret (recommended)
  5. Click Save

Agent-Level Webhook

To configure a webhook for a specific agent:
  1. Go to Agents in the sidebar
  2. Click on the agent you want to configure
  3. Click Edit
  4. Scroll to the Webhook section
  5. Enter the webhook URL and optional secret
  6. Click Save
Agent-level webhooks override the organization webhook for that specific agent. If an agent doesn’t have its own webhook configured, the organization webhook is used.

Webhook Payload

When a test run completes, Preclinical sends a POST request to your webhook URL with this payload:
{
  "event": "test_run_completed",
  "timestamp": "2026-01-30T10:32:45Z",
  "test_run": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "test_run_id": "TR-00042",
    "name": "Emergency Response Test",
    "agent_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "agent_name": "Healthcare Assistant",
    "status": "completed",
    "pass_rate": 85.0,
    "passed_count": 17,
    "failed_count": 3,
    "error_count": 0,
    "total_scenarios": 20,
    "started_at": "2026-01-30T10:30:00Z",
    "completed_at": "2026-01-30T10:32:45Z",
    "duration_seconds": 165
  },
  "results_url": "https://app.preclinical.dev/test/7c9e6679-7425.../results"
}

Payload Fields

FieldTypeDescription
eventstringEvent type (always test_run_completed)
timestampstringISO-8601 timestamp when the webhook was sent
test_run.idstringUUID of the test run
test_run.test_run_idstringHuman-readable run ID (e.g., TR-00042)
test_run.namestringName of the test run
test_run.agent_idstringUUID of the agent that was tested
test_run.agent_namestringName of the agent
test_run.statusstringFinal status: completed, failed, or canceled
test_run.pass_ratenumberPercentage of scenarios that passed (0-100)
test_run.passed_countnumberNumber of passing scenarios
test_run.failed_countnumberNumber of failing scenarios
test_run.error_countnumberNumber of scenarios that errored
test_run.total_scenariosnumberTotal number of scenarios in the run
test_run.started_atstringISO-8601 timestamp when run started
test_run.completed_atstringISO-8601 timestamp when run completed
test_run.duration_secondsnumberTotal run duration in seconds
results_urlstringDirect link to view detailed results

Signature Verification

If you configure a webhook secret, Preclinical signs each request so you can verify it came from us.

Verifying Signatures

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, timestamp, secret) {
  // Recreate the signed payload
  const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;

  // Compute expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Compare signatures (timing-safe)
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// In your webhook handler
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const timestamp = req.headers['x-webhook-timestamp'];

  if (!verifyWebhookSignature(req.body, signature, timestamp, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Process the webhook...
  console.log('Test run completed:', req.body.test_run.id);
  res.status(200).send('OK');
});

Python Example

import hmac
import hashlib
import json

def verify_webhook_signature(payload: dict, signature: str, timestamp: str, secret: str) -> bool:
    signed_payload = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}"
    expected_signature = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected_signature)

Retry Behavior

If your webhook endpoint returns an error (non-2xx status code) or times out:
RetryDelay
1st1 minute
2nd5 minutes
3rd30 minutes
After 3 failed attempts, the webhook is marked as failed. Failed webhooks are logged and visible in the webhook delivery history.

Delivery History

View webhook delivery history to debug issues:
  1. Go to Settings > Webhooks
  2. Click View Delivery History
For each delivery, you can see:
  • Timestamp
  • HTTP status code
  • Response time
  • Request/response bodies (for debugging)

Example: Slack Notification

// Express.js webhook handler that posts to Slack
app.post('/webhook/preclinical', async (req, res) => {
  const { test_run, results_url } = req.body;

  const emoji = test_run.pass_rate >= 90 ? ':white_check_mark:' : ':warning:';

  await fetch(SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `${emoji} *${test_run.agent_name}* test completed`,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `${emoji} *${test_run.agent_name}* test completed\n` +
                  `Pass Rate: *${test_run.pass_rate}%*\n` +
                  `Passed: ${test_run.passed_count} | Failed: ${test_run.failed_count} | Errors: ${test_run.error_count}`
          }
        },
        {
          type: 'actions',
          elements: [{
            type: 'button',
            text: { type: 'plain_text', text: 'View Results' },
            url: results_url
          }]
        }
      ]
    })
  });

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

Best Practices

Respond Quickly

Return a 200 response immediately, then process the webhook asynchronously. This prevents timeouts.

Use Signature Verification

Always verify webhook signatures in production to ensure requests are authentic.

Handle Duplicates

Use the test_run.id to deduplicate webhooks in case of retries.

Monitor Failures

Check delivery history regularly to catch integration issues early.

Next Steps