Webhooks

Receive real-time notifications when important events occur. Webhooks allow you to react immediately to blocked requests, failed challenges, and other security events.


Setting Up Webhooks

Configure Webhook

POST/v1/webhooks/config

Configure Webhook

Set up webhook notifications for your organization.

bash
curl -X POST "https://ag.runloci.com/v1/webhooks/config" \
  -H "x-org-id: your_org_id" \
  -H "x-api-key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-backend.com/webhooks/accessgate",
    "secret": "whsec_your_webhook_secret",
    "events": ["risk.high", "risk.low"],
    "enabled": true
  }'

Response: 200 OK

json
{
  "url": "https://your-backend.com/webhooks/accessgate",
  "events": ["risk.high", "risk.low"],
  "enabled": true,
  "created_at": "2026-01-25T10:30:00Z"
}

Available Events

Event Description When Triggered
risk.high High-risk request detected Score >= block threshold
risk.low Low-risk request Score below review threshold
challenge.passed Challenge completed User verified successfully
challenge.failed Challenge failed Wrong code or expired

Subscribe only to events you need. This reduces noise and improves webhook reliability.

---

Webhook Payload Format

All webhook events follow this format:

json
{
  "id": "evt_xyz789",
  "type": "risk.high",
  "created_at": "2026-01-25T10:30:00Z",
  "data": {
    "request_id": "req_abc123",
    "entity_id": "user_123",
    "decision": {
      "outcome": "block",
      "score": 87,
      "reasons": ["Impossible travel detected", "New device detected"]
    },
    "context": {
      "action": "login",
      "ip": "102.88.34.45",
      "email": "user@example.com"
    }
  }
}

Event Payloads

risk.high

json
{
  "id": "evt_001",
  "type": "risk.high",
  "created_at": "2026-01-25T10:30:00Z",
  "data": {
    "request_id": "req_abc123",
    "entity_id": "user_123",
    "decision": {
      "outcome": "block",
      "score": 87,
      "reasons": ["Impossible travel detected", "New device detected"]
    },
    "context": {
      "action": "login",
      "ip": "192.0.2.1",
      "email": "user@example.com",
      "session_id": "sess_xyz"
    }
  }
}

challenge.failed

json
{
  "id": "evt_003",
  "type": "challenge.failed",
  "created_at": "2026-01-25T10:35:00Z",
  "data": {
    "challenge_id": "ch_ghi789",
    "entity_id": "user_123",
    "type": "email_otp",
    "reason": "max_attempts_exceeded",
    "original_request_id": "req_abc123"
  }
}

Verifying Webhook Signatures

All webhooks are signed using HMAC-SHA256. Verify signatures to ensure webhooks are from AccessGate.

Signature Header

text
X-AccessGate-Signature: sha256=abc123def456...
X-AccessGate-Timestamp: 1706180400

Verification (Node.js)

javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, timestamp, secret) {
  const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expectedSignature}`)
  );
}

// Express middleware
app.post('/webhooks/accessgate', express.json(), (req, res) => {
  const signature = req.headers['x-accessgate-signature'];
  const timestamp = req.headers['x-accessgate-timestamp'];
  
  if (!verifyWebhookSignature(req.body, signature, timestamp, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Process webhook
  const event = req.body;
  console.log(`Received ${event.type} event`);
  
  res.status(200).json({ received: true });
});

Verification (Python)

python
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = 'whsec_your_secret'

def verify_signature(payload, signature, timestamp, secret):
    signed_payload = f"{timestamp}.{payload}"
    expected = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, f"sha256={expected}")

@app.route('/webhooks/accessgate', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-AccessGate-Signature')
    timestamp = request.headers.get('X-AccessGate-Timestamp')
    
    if not verify_signature(request.data.decode(), signature, timestamp, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401
    
    event = request.json
    print(f"Received {event['type']} event")
    
    return jsonify({'received': True}), 200

Always verify webhook signatures before processing. Never trust unverified webhooks.

---

Handling Webhooks

Best Practices

1. Respond Quickly Return a 200 status immediately. Process the event asynchronously.

javascript
app.post('/webhooks/accessgate', (req, res) => {
  // Acknowledge immediately
  res.status(200).json({ received: true });
  
  // Process asynchronously
  processWebhookAsync(req.body);
});

async function processWebhookAsync(event) {
  switch (event.type) {
    case 'risk.high':
      await notifySecurityTeam(event.data);
      break;
    case 'challenge.failed':
      await lockAccount(event.data.entity_id);
      break;
  }
}

2. Handle Retries If your endpoint returns a non-2xx status, we'll retry:

Attempt Delay
1 Immediate
2 5 seconds
3 30 seconds
4 2 minutes
5 10 minutes

Use idempotency keys to prevent duplicate processing:

javascript
const processedEvents = new Set();

app.post('/webhooks/accessgate', (req, res) => {
  const eventId = req.body.id;
  
  if (processedEvents.has(eventId)) {
    return res.status(200).json({ received: true, duplicate: true });
  }
  
  processedEvents.add(eventId);
  // Process event...
  
  res.status(200).json({ received: true });
});

3. Log Everything

javascript
app.post('/webhooks/accessgate', (req, res) => {
  console.log({
    event_id: req.body.id,
    event_type: req.body.type,
    timestamp: req.body.created_at,
    entity_id: req.body.data?.entity_id
  });
  
  res.status(200).json({ received: true });
});

Testing Webhooks

Using ngrok for Local Development

bash
# Terminal 1: Start your server
node server.js

# Terminal 2: Expose with ngrok
ngrok http 3000

Then configure the ngrok URL as your webhook endpoint:

bash
curl -X POST "https://ag-staging.runloci.com/v1/webhooks/config" \
  -H "x-org-id: your_org_id" \
  -H "x-api-key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/accessgate",
    "events": ["risk.high"],
    "enabled": true
  }'

Support