Skip to main content

Custom Webhook Integration

Overview

The Custom Webhook integration allows you to implement your own guardrails logic by providing a webhook endpoint that validates tool calls. When enabled, Webrix will send validation requests to your webhook at two critical stages: before tool execution (input validation) and after tool execution (output validation).

This gives you complete control over your security and compliance requirements, allowing you to:

  • Validate Tool Inputs: Screen tool arguments before they're executed to detect policy violations, sensitive data, or inappropriate requests
  • Validate Tool Outputs: Review tool responses before they reach users to ensure compliance and safety
  • Custom Business Logic: Implement organization-specific rules, rate limiting, content filtering, or any other custom validation logic
  • Observe and Debug: Use "Observe Only Mode" to test your webhook without blocking any calls

How It Works

When Custom Webhook guardrails are enabled, the flow works as follows:

  1. Input Stage (Before Execution):

    • User initiates a tool call through an AI client
    • Webrix sends a POST request to your webhook with tool metadata and arguments
    • Your webhook analyzes the request and responds with an action block, allow, warn, or transform.
    • If blocked, the tool is not executed and an error is returned to the user
  2. Output Stage (After Execution):

    • Tool executes successfully (only if input stage allowed it)
    • Webrix sends a POST request to your webhook with the tool results
    • Your webhook analyzes the response and decides whether to block it
    • If blocked, the results are not returned to the user

Webhook Request Format

Your webhook will receive POST requests with the following JSON payload:

{
"action": "tool_call",
"stage": "input",
"user": {
"email": "[email protected]"
},
"mcpClient": "claude-desktop",
"toolkit": "mcp-toolkit",
"transport": "http",
"connector": {
"slug": "slack-connector",
"display": {
"name": "Slack",
"description": "Slack integration",
"icon": "slack.png"
},
"status": "active"
},
"integration": {
"slug": "slack-integration",
"name": "Slack",
"description": "Connect to Slack",
"connectorId": "slack",
"authType": "oauth2"
},
"tool": {
"name": "send_message",
"arguments": {
"channel": "#general",
"text": "Hello team!"
}
},
"payload": {
"channel": "#general",
"text": "Hello team!"
}
}

Request Fields

FieldTypeRequiredDescription
action"tool_call"YesAlways "tool_call" (reserved for future actions)
stage"input" | "output"Yes"input" for pre-execution, "output" for post-execution
user.emailstringYesEmail of the user making the request
mcpClientstringNoThe AI client being used (e.g., "claude-desktop", "cursor")
toolkitstringNoThe toolkit identifier
transport"stdio" | "http"NoThe transport protocol being used
connectorobjectNoInformation about the connector
connector.slugstringNoUnique identifier for the connector
connector.displayobjectNoDisplay information for the connector
connector.statusstringNoStatus of the connector
integrationobjectNoInformation about the integration
integration.slugstringNoUnique identifier for the integration
integration.namestringNoHuman-readable integration name
integration.descriptionstringNoIntegration description
integration.connectorIdstringNoThe connector type
integration.authTypestringNoAuthentication method used
toolobjectNoInformation about the tool being called
tool.namestringNoName of the tool being called
tool.argumentsobjectNoArguments passed to the tool
payloadanyNoThe data being sent (input stage) or received (output stage) from the tool

Output Stage Example

When stage is "output", the payload includes the tool's results:

{
"action": "tool_call",
"stage": "output",
"user": {
"email": "[email protected]"
},
"mcpClient": "claude-desktop",
"toolkit": "mcp-toolkit",
"transport": "http",
"connector": {
"slug": "slack-connector",
"display": {
"name": "Slack",
"description": "Slack integration",
"icon": "slack.png"
},
"status": "active"
},
"integration": {
"slug": "slack-integration",
"name": "Slack",
"description": "Connect to Slack",
"connectorId": "slack",
"authType": "oauth2"
},
"tool": {
"name": "get_channel_history",
"arguments": {
"channel": "#private-channel"
}
},
"payload": {
"messages": [
{
"user": "U123",
"text": "Confidential project discussion",
"ts": "1234567890.123456"
}
]
}
}

Webhook Response Format

Your webhook must respond with a JSON object:

{
"action": "allow",
"payload": null,
"logData": {
"code": "validation_passed",
"details": { "checks": ["pii", "rate_limit"] }
}
}

Response Fields

FieldTypeRequiredDescription
action"block" | "allow" | "transform" | "warn"YesThe action to take on this request
payloadanyNoRequired when action="transform". The transformed data to use
logDataobjectNoCustom logging data to store in audit logs
logData.codestringNoA code identifying the log entry (e.g., "pii_detected")
logData.detailsanyNoAdditional data to log

Action Types

  • allow: Allow the request to proceed with the original payload
  • block: Block the request and return an error to the user
  • transform: Transform the payload (requires payload field)
  • warn: Allow the request but log a warning for review

Response Time

  • Your webhook timeout is configurable (default: 2.5 seconds)
  • If your webhook takes longer or is unreachable, the fail-open/fail-closed setting determines behavior
  • Fail Open (recommended): Allow the call if webhook fails
  • Fail Closed: Block the call if webhook fails
  • In "Observe Only Mode", responses are never waited for

Configuration

Prerequisites

Before setting up the Custom Webhook integration, ensure you have:

  1. A publicly accessible webhook endpoint (HTTPS required for production)
  2. The ability to process POST requests and respond within 2.5 seconds
  3. Admin access to your Webrix dashboard

Setup Instructions

Step 1: Implement Your Webhook

Create a webhook endpoint that:

  1. Accepts POST requests with the JSON payload described above
  2. Implements your validation logic
  3. Returns a JSON response with the Webhook Response Format
  4. Responds within 2.5 seconds

Example implementation (Node.js/Express):

app.post("/validate-tool-call", async (req, res) => {
const { action, stage, user, integration, tool, payload } = req.body

// Example: Block sensitive Slack channels
if (
integration.connectorId === "slack" &&
tool.arguments?.channel === "#executive-only"
) {
return res.json({
action: "block",
logData: {
code: "sensitive_channel",
details: { channel: tool.arguments.channel },
},
})
}

// Example: Mask PII in outputs
if (stage === "output" && containsPII(payload)) {
const maskedPayload = maskPII(payload)
return res.json({
action: "transform",
payload: maskedPayload,
logData: {
code: "pii_masked",
details: { fieldsModified: ["email", "ssn"] },
},
})
}

// Example: Warn about large responses
if (stage === "output" && JSON.stringify(payload).length > 10000) {
return res.json({
action: "warn",
logData: {
code: "large_response",
details: { size: JSON.stringify(payload).length },
},
})
}

// Allow by default
res.json({ action: "allow" })
})

Step 2: Enable in Webrix

  1. Log in to your Webrix admin panel
  2. Navigate to SettingsAdvanced Security Settings
  3. Toggle on Enable Custom Webhook Integration
  4. Enter your webhook URL (e.g., https://your-domain.com/validate-tool-call)
  5. Configure Timeout (default: 2500ms) - maximum time to wait for webhook response
  6. Configure Fail Open (recommended) - allow calls when webhook times out or errors
  7. (Optional) Enable Observe Only Mode for testing
  8. Click Save Changes

Step 3: Test Your Integration

Start with Observe Only Mode enabled:

  1. Your webhook will receive real requests
  2. But responses will be ignored (no calls will be blocked)
  3. Use this to test your webhook logic and ensure it responds correctly
  4. Monitor your webhook logs to verify requests are being received

Once confident:

  1. Disable Observe Only Mode
  2. Your webhook responses will now control whether calls are blocked
  3. Monitor your webhook and Webrix logs for any issues

Observe Only Mode

Observe Only Mode is designed for testing and debugging:

  • Webhook requests are sent but responses are not awaited
  • No calls will ever be blocked, regardless of your webhook's response
  • Perfect for:
    • Initial testing of your webhook
    • Logging and analytics without affecting users
    • Debugging validation logic in production

When to Use Observe Only Mode

  • ✅ Testing a new webhook implementation
  • ✅ Monitoring tool usage without enforcement
  • ✅ Collecting data to build validation rules
  • ✅ Debugging issues without impacting users

When to Disable Observe Only Mode

  • ✅ Webhook is tested and working correctly
  • ✅ Validation logic is finalized
  • ✅ Ready to enforce guardrails in production

Use Cases

PII Masking (Transform Action)

Detect and mask personally identifiable information in outputs:

function maskPII(payload) {
const payloadStr = JSON.stringify(payload)

// Mask SSN
let masked = payloadStr.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "XXX-XX-XXXX")

// Mask credit cards
masked = masked.replace(/\b\d{16}\b/g, "XXXX-XXXX-XXXX-XXXX")

// Mask emails
masked = masked.replace(
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
"[EMAIL_REDACTED]",
)

return JSON.parse(masked)
}

app.post("/validate-tool-call", async (req, res) => {
const { stage, payload } = req.body

if (stage === "output") {
const hasPII = detectPII(payload)
if (hasPII) {
return res.json({
action: "transform",
payload: maskPII(payload),
logData: {
code: "pii_masked",
details: { types: ["email", "ssn", "credit_card"] },
},
})
}
}

res.json({ action: "allow" })
})

Content Filtering (Block Action)

Block tool calls containing specific keywords or patterns:

function shouldBlockContent(tool, payload, stage) {
const bannedWords = ["confidential", "secret", "password"]
const content =
stage === "input" ? JSON.stringify(tool.arguments) : JSON.stringify(payload)

return bannedWords.some((word) => content.toLowerCase().includes(word))
}

app.post("/validate-tool-call", async (req, res) => {
const { tool, payload, stage } = req.body

if (shouldBlockContent(tool, payload, stage)) {
return res.json({
action: "block",
logData: {
code: "banned_content",
details: { stage },
},
})
}

res.json({ action: "allow" })
})

Response Filtering (Transform Action)

Reduce large API responses to save token costs:

app.post("/validate-tool-call", async (req, res) => {
const { stage, payload, tool } = req.body

if (stage === "output" && tool.name === "list_items") {
// Limit to first 50 items
if (Array.isArray(payload) && payload.length > 50) {
return res.json({
action: "transform",
payload: payload.slice(0, 50),
logData: {
code: "response_truncated",
details: { original: payload.length, filtered: 50 },
},
})
}
}

res.json({ action: "allow" })
})

Rate Limiting (Block Action)

Limit tool calls per user:

const rateLimits = new Map()

app.post("/validate-tool-call", async (req, res) => {
const { user } = req.body
const now = Date.now()
const userCalls = rateLimits.get(user.email) || []

// Keep only calls from last hour
const recentCalls = userCalls.filter((t) => now - t < 3600000)

if (recentCalls.length >= 100) {
return res.json({
action: "block",
logData: {
code: "rate_limit_exceeded",
details: { calls: recentCalls.length, limit: 100 },
},
})
}

rateLimits.set(user.email, [...recentCalls, now])
res.json({ action: "allow" })
})

Monitoring and Alerting (Warn Action)

Monitor suspicious activity without blocking:

app.post("/validate-tool-call", async (req, res) => {
const { tool, user, payload } = req.body

// Warn on sensitive operations
if (tool.name === "delete_all_data") {
// Send alert to security team
await sendSecurityAlert({
user: user.email,
tool: tool.name,
timestamp: new Date(),
})

return res.json({
action: "warn",
logData: {
code: "sensitive_operation",
details: { tool: tool.name, user: user.email },
},
})
}

res.json({ action: "allow" })
})

Business Hours Enforcement (Block Action)

Block tool calls outside business hours:

app.post("/validate-tool-call", async (req, res) => {
const now = new Date()
const hour = now.getHours()
const day = now.getDay()

// Block weekends and outside 9am-5pm
if (day === 0 || day === 6 || hour < 9 || hour >= 17) {
return res.json({
action: "block",
logData: {
code: "outside_business_hours",
details: { day, hour },
},
})
}

res.json({ action: "allow" })
})

Payload Transformation Best Practices

When using the transform action, keep these guidelines in mind:

1. Maintain Data Structure

Ensure transformed payloads maintain the expected structure:

// ❌ Bad: Changes structure
{ users: [...] }["user1", "user2"]

// ✅ Good: Maintains structure
{ users: [{ id: 1, email: "[email protected]" }] }
{ users: [{ id: 1, email: "[REDACTED]" }] }

2. Handle Nested Objects

Use recursive functions for deep transformation:

function maskPIIRecursive(obj) {
if (typeof obj !== "object" || obj === null) return obj

const result = Array.isArray(obj) ? [] : {}

for (const key in obj) {
if (key === "email" && typeof obj[key] === "string") {
result[key] = "[EMAIL_REDACTED]"
} else if (typeof obj[key] === "object") {
result[key] = maskPIIRecursive(obj[key])
} else {
result[key] = obj[key]
}
}

return result
}

3. Size Considerations

Large transformations may approach timeout limits:

// Optimize for speed
if (JSON.stringify(payload).length > 1000000) {
// Quick filter instead of deep transformation
return res.json({
action: "allow",
logData: {
code: "payload_too_large_to_transform",
details: { size: JSON.stringify(payload).length },
},
})
}

4. Test Transformations

Always validate that your transformations work:

const original = { data: [...] }
const transformed = transformPayload(original)

// Ensure structure is preserved
assert(typeof transformed === typeof original)
assert(Array.isArray(transformed.data) === Array.isArray(original.data))

LogData and Audit Trail

The logData field allows you to store custom information in Webrix audit logs:

Log Structure

{
"code": "pii_detected", // Optional: classification code
"details": { // Optional: additional context
"fields": ["email", "ssn"],
"count": 2,
"action_taken": "masked"
}
}

Viewing Logs

LogData entries are stored in the audit logs and include:

  • Stage (input/output)
  • Timestamp
  • Your custom code
  • Your custom data

Best Practices

  1. Use consistent codes: Define a standard set of codes ("pii_detected", "rate_limited", etc.)
  2. Include context: Add useful debugging information in the details field
  3. Don't log sensitive data: Avoid logging the actual PII or sensitive content
  4. Keep it concise: Large log entries affect database performance

Example Log Codes

const LOG_CODES = {
PII_DETECTED: "pii_detected",
PII_MASKED: "pii_masked",
RATE_LIMITED: "rate_limited",
LARGE_RESPONSE: "large_response_filtered",
SUSPICIOUS: "suspicious_activity",
BUSINESS_HOURS: "outside_business_hours",
}

Best Practices

Performance

  • Respond Quickly: Configure timeout appropriately (default: 2.5s, increase for transformations)
  • Use Caching: Cache validation results when appropriate
  • Async Processing: For complex logic, log async and respond quickly
  • Monitor Timeouts: Track how often your webhook times out
  • Transformation Cost: Heavy transformations may need longer timeouts

Security

  • Use HTTPS: Always use HTTPS for your webhook endpoint
  • Validate Requests: Consider adding request signature verification
  • Rate Limit: Protect your webhook from abuse
  • Secure Storage: Store any secrets or API keys securely

Reliability

  • Handle Errors: Implement proper error handling
  • Log Everything: Keep detailed logs for debugging
  • Monitor Uptime: Ensure your webhook is highly available
  • Fail Safe: Remember that timeouts/errors fail open (allow calls)

Testing

  • Start with Observe Only: Test thoroughly before blocking real calls
  • Test Both Stages: Verify both input and output validation
  • Test Edge Cases: Try various tool types and scenarios
  • Monitor Initially: Watch logs closely after enabling

Troubleshooting

Webhook Not Receiving Requests

  1. Verify URL: Ensure your webhook URL is correct and publicly accessible
  2. Check HTTPS: Confirm your endpoint uses HTTPS
  3. Test Endpoint: Use curl or Postman to test your endpoint
  4. Check Firewall: Ensure no firewall is blocking Webrix's requests

Calls Not Being Blocked

  1. Check Observe Only Mode: Ensure it's disabled if you want to block calls
  2. Verify Response: Confirm your webhook returns {"action": "block"}
  3. Check Response Time: Ensure you respond within configured timeout
  4. Check Fail Open Setting: If enabled, timeouts will allow calls
  5. Review Logs: Check Webrix and webhook logs for errors

Timeouts or Slow Performance

  1. Optimize Logic: Reduce computation time in your webhook
  2. Use Caching: Cache validation results when possible
  3. Increase Resources: Scale up your webhook server if needed
  4. Consider Async: For heavy operations, log async and respond quickly

False Positives

  1. Review Logic: Check if validation rules are too strict
  2. Add Exceptions: Whitelist known-safe patterns
  3. Use Observe Only: Test refined logic before enforcing
  4. Collect Data: Use observe mode to gather real-world examples

Error Handling

When Webhook Fails

Webrix behavior depends on your Fail Open setting:

Fail Open (Recommended):

  • Webhook unreachable → Call is allowed
  • Webhook times out → Call is allowed
  • Webhook returns error → Call is allowed
  • Ensures availability even if webhook has issues

Fail Closed (Strict Security):

  • Webhook unreachable → Call is blocked
  • Webhook times out → Call is blocked
  • Webhook returns error → Call is blocked
  • Maximizes security but impacts availability

Validation Errors

Invalid webhook responses are handled based on fail-open setting:

  • Missing action field → Treated as webhook error
  • action: "transform" without payload → Treated as webhook error
  • Invalid action value → Treated as webhook error

In Observe Only Mode

  • All calls are always allowed
  • Webhook errors are logged but don't affect users
  • Perfect for testing without risk

FAQ

Can I use different logic for input vs output stages?

Yes! Check the stage field in the webhook payload to implement different validation logic for each stage.

What happens if my webhook is down?

Webrix will fail open (allow the call). This ensures your MCP remains functional even if your webhook has issues.

Can I block some users but not others?

Yes! Use the user.email field to implement user-specific rules.

Can I customize the error message shown to users?

Currently, blocked calls return a standard "Tool blocked by organization's guardrails" message. Custom messages may be added in future versions.

What's the difference between "block" and "warn" actions?

  • block: Stops execution and returns an error to the user
  • warn: Allows execution but logs the event for review. Useful for monitoring without disruption.

Can I use both transformation and logging?

Yes! You can return both a transformed payload and logData:

{
"action": "transform",
"payload": maskedData,
"logData": {
"code": "pii_masked",
"details": { "fields": ["email"] }
}
}

Does this affect all integrations?

Yes, when enabled, all tool calls across all integrations are validated through your webhook.

Can I disable the webhook temporarily?

Yes, simply toggle off "Enable Custom Webhook Integration" in the settings. Changes take effect immediately.

What's the difference between this and Active Fence?

  • Active Fence: Analyzes prompt text content for safety/compliance
  • Custom Webhook: Gives you full control to implement any validation logic you need
  • You can use both simultaneously for defense in depth

Support

For additional support:

  • Webrix Support: Contact your Webrix administrator or support team
  • Documentation: Check the Webrix documentation for updates
  • Community: Join the Webrix community for tips and examples