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:
-
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, ortransform. - If blocked, the tool is not executed and an error is returned to the user
-
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
| Field | Type | Required | Description |
|---|---|---|---|
action | "tool_call" | Yes | Always "tool_call" (reserved for future actions) |
stage | "input" | "output" | Yes | "input" for pre-execution, "output" for post-execution |
user.email | string | Yes | Email of the user making the request |
mcpClient | string | No | The AI client being used (e.g., "claude-desktop", "cursor") |
toolkit | string | No | The toolkit identifier |
transport | "stdio" | "http" | No | The transport protocol being used |
connector | object | No | Information about the connector |
connector.slug | string | No | Unique identifier for the connector |
connector.display | object | No | Display information for the connector |
connector.status | string | No | Status of the connector |
integration | object | No | Information about the integration |
integration.slug | string | No | Unique identifier for the integration |
integration.name | string | No | Human-readable integration name |
integration.description | string | No | Integration description |
integration.connectorId | string | No | The connector type |
integration.authType | string | No | Authentication method used |
tool | object | No | Information about the tool being called |
tool.name | string | No | Name of the tool being called |
tool.arguments | object | No | Arguments passed to the tool |
payload | any | No | The 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
| Field | Type | Required | Description |
|---|---|---|---|
action | "block" | "allow" | "transform" | "warn" | Yes | The action to take on this request |
payload | any | No | Required when action="transform". The transformed data to use |
logData | object | No | Custom logging data to store in audit logs |
logData.code | string | No | A code identifying the log entry (e.g., "pii_detected") |
logData.details | any | No | Additional data to log |
Action Types
allow: Allow the request to proceed with the original payloadblock: Block the request and return an error to the usertransform: Transform the payload (requirespayloadfield)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:
- A publicly accessible webhook endpoint (HTTPS required for production)
- The ability to process POST requests and respond within 2.5 seconds
- Admin access to your Webrix dashboard
Setup Instructions
Step 1: Implement Your Webhook
Create a webhook endpoint that:
- Accepts POST requests with the JSON payload described above
- Implements your validation logic
- Returns a JSON response with the
Webhook Response Format - 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
- Log in to your Webrix admin panel
- Navigate to Settings → Advanced Security Settings
- Toggle on Enable Custom Webhook Integration
- Enter your webhook URL (e.g.,
https://your-domain.com/validate-tool-call) - Configure Timeout (default: 2500ms) - maximum time to wait for webhook response
- Configure Fail Open (recommended) - allow calls when webhook times out or errors
- (Optional) Enable Observe Only Mode for testing
- Click Save Changes
Step 3: Test Your Integration
Start with Observe Only Mode enabled:
- Your webhook will receive real requests
- But responses will be ignored (no calls will be blocked)
- Use this to test your webhook logic and ensure it responds correctly
- Monitor your webhook logs to verify requests are being received
Once confident:
- Disable Observe Only Mode
- Your webhook responses will now control whether calls are blocked
- 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
- Use consistent codes: Define a standard set of codes ("pii_detected", "rate_limited", etc.)
- Include context: Add useful debugging information in the details field
- Don't log sensitive data: Avoid logging the actual PII or sensitive content
- 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
- Verify URL: Ensure your webhook URL is correct and publicly accessible
- Check HTTPS: Confirm your endpoint uses HTTPS
- Test Endpoint: Use curl or Postman to test your endpoint
- Check Firewall: Ensure no firewall is blocking Webrix's requests
Calls Not Being Blocked
- Check Observe Only Mode: Ensure it's disabled if you want to block calls
- Verify Response: Confirm your webhook returns
{"action": "block"} - Check Response Time: Ensure you respond within configured timeout
- Check Fail Open Setting: If enabled, timeouts will allow calls
- Review Logs: Check Webrix and webhook logs for errors
Timeouts or Slow Performance
- Optimize Logic: Reduce computation time in your webhook
- Use Caching: Cache validation results when possible
- Increase Resources: Scale up your webhook server if needed
- Consider Async: For heavy operations, log async and respond quickly
False Positives
- Review Logic: Check if validation rules are too strict
- Add Exceptions: Whitelist known-safe patterns
- Use Observe Only: Test refined logic before enforcing
- 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
actionfield → Treated as webhook error action: "transform"withoutpayload→ 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