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", "target_user_email": "user@example.com", "context": "Access request for sensitive system", "callback_url": "https://your-system.com/webhook/callback", "requester_id": "system-name"}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”)
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"}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": "user@example.com", "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", "target_user_email": "user@example.com", "context": "Security verification required", "callback_url": "https://your-system.com/webhook/callback"}Fields:
integration_type: Must be"slack"slack_user_id(optional): Slack user ID of the requester. If not provided,requester_emailmust be provided.requester_email(optional): Email of the requester. If not provided,slack_user_idmust be provided. The system will automatically look up the Slack user ID.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 URLslack_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 as they’re easier for external systems to provide - the app will automatically look up Slack user IDs.
Example cURL:
# Using emails (recommended - easier for external systems)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": "requester@example.com", "target_user_email": "user@example.com", "context": "Security verification required" }'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 challenge completes, the system will send a POST request to your configured callback URL (either the integration default or the per-request callback_url).
Callback Payload
HTTP Method: POST
Content-Type: application/json
Headers:
X-Webhook-Signature(if webhook secret is configured):sha256=<signature>
Payload:
{ "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", "target_user_email": "user@example.com", "context": "Access request for sensitive system", "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"}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": "user@example.com", "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": "user@example.com", "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 support@veraproof.io or refer to the main documentation.