Webhook Integration
Webhook Integration
This guide explains how to use the webhook integration to create identity verification challenges programmatically and receive callbacks when challenges complete.
Overview
The webhook integration allows external systems to:
- Create identity verification challenges via REST API
- Create Slack-based challenges programmatically (alternative to slash commands)
- Check challenge status via API
- Receive outbound webhook callbacks when challenges complete
Setup
1. Create a Webhook Integration
- Log in to the Challenge admin console at challenge.veraproof.io
- Navigate to Integrations → Webhook Integrations
- Click Add Webhook Integration
- Fill in:
- Display Name: A friendly name (e.g., “ITSM System”)
- Callback URL (Optional): URL to receive webhook callbacks when challenges complete
- Webhook Secret (Optional): Secret for signing outbound webhooks (HMAC SHA256)
- Click Create Integration
- Copy the API Key - it will only be shown once!
2. Get Your API Key
The API key is generated automatically when you create a webhook integration. If you lose it:
- Go to Integrations → Webhook Integrations
- Find your integration
- Click Regenerate API Key
- Copy the new key immediately - it won’t be shown again
Authentication
All webhook API requests require authentication using an API key. Include the API key in one of two ways:
Option 1: X-API-Key Header (Recommended)
X-API-Key: your-api-key-hereOption 2: Authorization Bearer Header
Authorization: Bearer your-api-key-hereCreating Challenges
Standard Webhook Challenge
Creates a standard identity verification challenge that returns a verification URL.
Endpoint: POST /api/v1/challenges
Request Body:
{ "target_user_id": "user123", "context": "Access request for sensitive system", "callback_url": "https://your-system.com/webhook/callback", "requester_id": "system-name", "expiry_minutes": 30}Fields:
target_user_id(optional): Target user identifiertarget_user_email(optional): Target user email (required iftarget_user_idnot provided)context(optional): Reason/message for the challengecallback_url(optional): Per-request callback URL (overrides integration default)requester_id(optional): Requester identifier (defaults to “webhook”)expiry_minutes(optional): Per-challenge lifetime in minutes (1–10080). Omit to use the tenant default. See Challenge expiry.
Response:
{ "challenge_id": "550e8400-e29b-41d4-a716-446655440000", "verification_url": "https://challenge.veraproof.io/challenge/start/550e8400-e29b-41d4-a716-446655440000", "status": "initiated", "expires_at": "2025-12-01T12:00:00Z"}The expires_at timestamp reflects the effective window for this challenge: per-request expiry_minutes when provided, otherwise your tenant default (see Challenge expiry).
Example cURL:
curl -X POST https://challenge.veraproof.io/api/v1/challenges \ -H "X-API-Key: your-api-key-here" \ -H "Content-Type: application/json" \ -d '{ "target_user_email": "[email protected]", "context": "Access request for production database", "callback_url": "https://your-system.com/webhook/callback" }'Slack Challenge via Webhook
Creates a Slack-based challenge programmatically (alternative to using the /challenge slash command).
Endpoint: POST /api/v1/challenges
Request Body:
{ "integration_type": "slack", "slack_user_id": "U01234ABCD", "target_user_id": "U05678EFGH", "context": "Security verification required", "callback_url": "https://your-system.com/webhook/callback", "expiry_minutes": 30}Fields:
integration_type: Must be"slack"slack_user_id(optional): Slack user ID of the requester. If omitted,requester_emailmay be provided for lookup.requester_email(optional): Email of the requester; the system looks up the Slack user ID. If neitherslack_user_idnorrequester_emailis provided, the Slack app’s bot user is used as the requester (resolved dynamically viaauth.test).target_user_id(optional): Target Slack user ID. If not provided,target_user_emailmust be provided.target_user_email(optional): Target user email. If not provided,target_user_idmust be provided. The system will automatically look up the Slack user ID.context(optional): Reason/message for the challengecallback_url(optional): Per-request callback URLexpiry_minutes(optional): Per-challenge lifetime in minutes (1–10080). Omit to use the tenant default.slack_instance_id(optional): Specific Slack integration instance ID to use. If not provided, the first available Slack integration for the tenant will be used.
Note: You can use any API key (webhook or Slack integration) to create Slack challenges, as long as your tenant has at least one Slack workspace configured. The system will automatically find and use the appropriate Slack integration for your tenant. Email addresses are preferred when you need a human requester—the app looks up Slack user IDs automatically. For automation (MCP, SOAR, ITSM) where no requester is known, omit both slack_user_id and requester_email and the Veraproof Challenge bot user will appear as the requester in Slack messages.
If you send a requester_email and that email is not present in the selected Slack workspace, the API returns an error. In that case, either provide a valid slack_user_id/requester_email or omit requester fields to use the bot requester behavior.
Example cURL:
# Using emails (recommended when a human requester is known)curl -X POST https://challenge.veraproof.io/api/v1/challenges \ -H "X-API-Key: your-webhook-api-key" \ -H "Content-Type: application/json" \ -d '{ "integration_type": "slack", "requester_email": "[email protected]", "target_user_email": "[email protected]", "context": "Security verification required" }'
# Automation: no requester — bot user is used as requester in Slackcurl -X POST https://challenge.veraproof.io/api/v1/challenges \ -H "X-API-Key: your-webhook-api-key" \ -H "Content-Type: application/json" \ -d '{ "integration_type": "slack", "target_user_email": "[email protected]", "context": "Automated security check" }'Checking Challenge Status
Check the current status of a challenge.
Endpoint: GET /api/v1/challenges/<challenge_id>
Response:
{ "challenge_id": "550e8400-e29b-41d4-a716-446655440000", "status": "verified", "created_at": "2025-12-01T10:00:00Z", "expires_at": "2025-12-01T10:15:00Z", "completed_at": "2025-12-01T10:05:00Z", "is_expired": false, "device_fingerprint": { "browser": "Chrome", "os": "Windows", "screen": "1920x1080", "ip": "203.0.113.1" }}Status Values:
initiated: Challenge created but not startedin-progress: User has started the verification processverified: Challenge completed successfullyfailed: Challenge failedexpired: Challenge expired
Example cURL:
curl -X GET https://challenge.veraproof.io/api/v1/challenges/550e8400-e29b-41d4-a716-446655440000 \ -H "X-API-Key: your-api-key-here"Receiving Webhook Callbacks
When a webhook-origin challenge completes, the system sends a POST request to your configured callback URL (either the integration default or the per-request callback_url).
Slack-origin challenges use Slack-native requester notifications instead of webhook callbacks.
Callback Payload
HTTP Method: POST
Content-Type: application/json
Headers:
X-Webhook-Signature(if webhook secret is configured):sha256=<signature>
Payload:
{ "event_type": "challenge.completed", "challenge_id": "550e8400-e29b-41d4-a716-446655440000", "status": "verified", "created_at": "2025-12-01T10:00:00Z", "completed_at": "2025-12-01T10:05:00Z", "expires_at": "2025-12-01T10:15:00Z", "target_user_id": "user123", "context": "Access request for sensitive system", "target_response": "I am traveling and using a new device.", "device_fingerprint": { "browser": "Chrome 120.0", "os": "Windows 11", "screen": "1920x1080", "timezone": "America/New_York", "ip": "203.0.113.1", "user_agent": "Mozilla/5.0..." }, "auth_method": "saml"}For incident-report actions, event_type is challenge.incident_reported.
Callback URL Priority
- Per-request callback URL (if provided in challenge creation)
- Integration default callback URL (if configured)
- No callback (if neither is configured)
Webhook Signature Verification
If you configured a webhook secret in your integration settings, all outbound webhooks will include an X-Webhook-Signature header for verification.
Signature Format
X-Webhook-Signature: sha256=<hex_signature>Verification Process
- Extract the signature from the
X-Webhook-Signatureheader - Remove the
sha256=prefix - Compute HMAC SHA256 of the request body using your webhook secret
- Compare the computed signature with the provided signature using constant-time comparison
Example Verification (Python)
import hmacimport hashlib
def verify_webhook_signature(payload, signature_header, secret): """ Verify webhook signature.
Args: payload: Raw request body (bytes or string) signature_header: Value from X-Webhook-Signature header secret: Your webhook secret
Returns: bool: True if signature is valid """ if not secret or not signature_header: return False
# Extract signature (remove "sha256=" prefix) if signature_header.startswith('sha256='): provided_signature = signature_header[7:] else: provided_signature = signature_header
# Convert payload to bytes if string if isinstance(payload, str): payload = payload.encode('utf-8')
# Compute expected signature expected_signature = hmac.new( secret.encode('utf-8'), payload, hashlib.sha256 ).hexdigest()
# Use constant-time comparison to prevent timing attacks return hmac.compare_digest(expected_signature, provided_signature)
# Example usage (Flask)from flask import request
@app.route('/webhook/callback', methods=['POST'])def webhook_callback(): signature = request.headers.get('X-Webhook-Signature') payload = request.get_data() secret = 'your-webhook-secret'
if not verify_webhook_signature(payload, signature, secret): return {'error': 'Invalid signature'}, 401
data = request.get_json() # Process webhook data... return {'status': 'ok'}, 200Example Verification (Node.js)
const crypto = require('crypto');
function verifyWebhookSignature(payload, signatureHeader, secret) { if (!secret || !signatureHeader) { return false; }
// Extract signature (remove "sha256=" prefix) const providedSignature = signatureHeader.replace('sha256=', '');
// Compute expected signature const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex');
// Use constant-time comparison return crypto.timingSafeEqual( Buffer.from(expectedSignature), Buffer.from(providedSignature) );}
// Example usage (Express)app.post('/webhook/callback', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; const payload = req.body; const secret = 'your-webhook-secret';
if (!verifyWebhookSignature(payload, signature, secret)) { return res.status(401).json({ error: 'Invalid signature' }); }
const data = JSON.parse(payload); // Process webhook data... res.json({ status: 'ok' });});Examples
Complete Workflow Example
# 1. Create a challengeRESPONSE=$(curl -X POST https://challenge.veraproof.io/api/v1/challenges \ -H "X-API-Key: your-api-key-here" \ -H "Content-Type: application/json" \ -d '{ "target_user_email": "[email protected]", "context": "Access request for production database", "callback_url": "https://your-system.com/webhook/callback" }')
# Extract challenge_id from responseCHALLENGE_ID=$(echo $RESPONSE | jq -r '.challenge_id')VERIFICATION_URL=$(echo $RESPONSE | jq -r '.verification_url')
echo "Challenge created: $CHALLENGE_ID"echo "Verification URL: $VERIFICATION_URL"
# 2. Share the verification URL with the user# (via email, ITSM ticket, Slack message, etc.)
# 3. Poll for status (optional)while true; do STATUS=$(curl -s -X GET \ https://challenge.veraproof.io/api/v1/challenges/$CHALLENGE_ID \ -H "X-API-Key: your-api-key-here" | jq -r '.status')
echo "Challenge status: $STATUS"
if [ "$STATUS" = "verified" ] || [ "$STATUS" = "failed" ] || [ "$STATUS" = "expired" ]; then break fi
sleep 5done
# 4. Or wait for webhook callback at your callback URLITSM Integration Example
# Create challenge and add verification URL to ITSM ticketcurl -X POST https://challenge.veraproof.io/api/v1/challenges \ -H "X-API-Key: your-api-key-here" \ -H "Content-Type: application/json" \ -d '{ "target_user_email": "[email protected]", "context": "ITSM Ticket #12345 - Access request", "callback_url": "https://itsm.example.com/api/webhooks/challenge-complete", "requester_id": "itsm-system" }' | jq -r '.verification_url' | \ xargs -I {} curl -X POST https://itsm.example.com/api/tickets/12345/comments \ -H "Authorization: Bearer itsm-token" \ -H "Content-Type: application/json" \ -d "{\"comment\": \"Please complete identity verification: {}\"}"Error Responses
401 Unauthorized
{ "error": "Invalid or missing API key"}400 Bad Request
{ "error": "target_user_id or target_user_email is required"}404 Not Found
{ "error": "Challenge not found"}500 Internal Server Error
{ "error": "Failed to create challenge"}Best Practices
- Store API keys securely: Never commit API keys to version control
- Use HTTPS: Always use HTTPS for callback URLs
- Verify signatures: Always verify webhook signatures if a secret is configured
- Handle timeouts: Webhook callbacks have a 10-second timeout
- Idempotency: Handle duplicate webhook callbacks gracefully
- Error handling: Implement retry logic for failed webhook deliveries
- Logging: Log all webhook events for audit purposes
Support
For issues or questions, contact [email protected] or refer to the main documentation.