🔔 Webhooks

Receive real-time notifications when events happen in your Venshack account. Webhooks push data to your server as events occur, so you don't need to poll the API.

Setting Up Webhooks

Webhooks are configured through the Developer Dashboard:

  1. Go to Settings → Webhooks in your dashboard
  2. Click Add Webhook Endpoint
  3. Enter your endpoint URL (must be HTTPS in production)
  4. Select the events you want to receive
  5. Copy and save the webhook secret for signature verification
⚠️ Security

Always use HTTPS endpoints in production. Store your webhook secret securely - you'll need it to verify that webhooks are genuinely from Venshack.

Webhook Payload

All webhooks are sent as HTTP POST requests with a JSON body:

{
  "event": "vending.completed",
  "timestamp": "2026-03-23T14:30:00.000Z",
  "organizationId": "org_abc123",
  "data": {
    // Event-specific data
  }
}

Request Headers

Header Description
Content-Type application/json
X-Venshack-Signature HMAC-SHA256 signature for verification
X-Venshack-Event Event type (e.g., vending.completed)
X-Venshack-Delivery-Id Unique delivery ID for deduplication
X-Venshack-Timestamp Unix timestamp when the webhook was sent

Verifying Signatures

Always verify webhook signatures to ensure requests are from Venshack. The signature is computed as:

HMAC-SHA256(timestamp + "." + rawBody, webhookSecret)
Node.js / Express
const crypto = require('crypto');

function verifyWebhookSignature(req, webhookSecret) {
  const signature = req.headers['x-venshack-signature'];
  const timestamp = req.headers['x-venshack-timestamp'];
  const body = req.rawBody; // Raw request body as string

  // Check timestamp is within 5 minutes to prevent replay attacks
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${body}`;
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(signedPayload)
    .digest('hex');

  // Constant-time comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express middleware to capture raw body
app.use('/webhooks', express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

app.post('/webhooks/venshack', (req, res) => {
  if (!verifyWebhookSignature(req, process.env.VENSHACK_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { event, data } = req.body;

  switch (event) {
    case 'vending.completed':
      console.log(`Token purchased: ${data.token}`);
      break;
    case 'access_code.used':
      console.log(`Guest arrived: ${data.guestName}`);
      break;
    // Handle other events...
  }

  // Always respond with 200 to acknowledge receipt
  res.status(200).json({ received: true });
});
Python / Flask
import hmac
import hashlib
import time
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = 'your_webhook_secret'

def verify_signature(payload, signature, timestamp):
    # Check timestamp freshness (5 minutes)
    if abs(time.time() - int(timestamp)) > 300:
        return False
    
    signed_payload = f"{timestamp}.{payload}"
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(signature, expected)

@app.route('/webhooks/venshack', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Venshack-Signature')
    timestamp = request.headers.get('X-Venshack-Timestamp')
    payload = request.get_data(as_text=True)
    
    if not verify_signature(payload, signature, timestamp):
        return jsonify({'error': 'Invalid signature'}), 401
    
    data = request.json
    event = data['event']
    
    if event == 'vending.completed':
        print(f"Token: {data['data']['token']}")
    elif event == 'access_code.used':
        print(f"Guest: {data['data']['guestName']}")
    
    return jsonify({'received': True}), 200

Event Types

Vending Events

Event Description
vending.completed Electricity token purchased successfully
vending.failed Token purchase failed (funds refunded)
vending.completed payload
{
  "event": "vending.completed",
  "timestamp": "2026-03-23T14:30:00.000Z",
  "organizationId": "org_abc123",
  "data": {
    "transactionId": "txn_xyz789",
    "meterNumber": "1234567890123",
    "customerName": "John Doe",
    "amount": 50000,
    "token": "1234-5678-9012-3456-7890",
    "units": 25.5,
    "externalRef": "your-order-123"
  }
}

Access Code Events

Event Description
access_code.created New access code generated
access_code.used Access code verified at security gate
access_code.revoked Access code manually revoked
access_code.used payload
{
  "event": "access_code.used",
  "timestamp": "2026-03-23T14:30:00.000Z",
  "organizationId": "org_abc123",
  "data": {
    "accessCodeId": "ac_xyz789",
    "guestName": "Jane Smith",
    "guestPhone": "+2348012345678",
    "purpose": "Delivery",
    "usageCount": 1,
    "verifiedAt": "2026-03-23T14:30:00.000Z"
  }
}

Meter Events

Event Description
meter.created New meter registered
meter.approved Meter approved by admin
meter.rejected Meter rejected by admin
meter.deleted Meter removed from system

Responding to Webhooks

💡 Best Practice

Respond with a 2xx status code within 30 seconds to acknowledge receipt. Process the webhook asynchronously if needed.

Response Requirements

Retry Policy

Failed webhook deliveries are retried with exponential backoff:

Attempt Delay
1st retry 1 minute
2nd retry 5 minutes
3rd retry 30 minutes
4th retry 2 hours
5th retry 24 hours

After 5 failed attempts, the webhook is marked as failed.

Handling Duplicate Deliveries

Webhooks may be delivered more than once. Use the X-Venshack-Delivery-Id header to deduplicate:

// Store processed delivery IDs (e.g., in Redis with TTL)
const processedDeliveries = new Set();

app.post('/webhooks/venshack', async (req, res) => {
  const deliveryId = req.headers['x-venshack-delivery-id'];
  
  // Skip if already processed
  if (processedDeliveries.has(deliveryId)) {
    return res.status(200).json({ received: true, duplicate: true });
  }
  
  // Mark as processed BEFORE handling
  processedDeliveries.add(deliveryId);
  
  // Handle the webhook...
  await handleWebhook(req.body);
  
  res.status(200).json({ received: true });
});

Testing Webhooks

Test your webhook endpoint from the dashboard:

  1. Go to Settings → Webhooks
  2. Click the Test button next to your endpoint
  3. Select an event type to send
  4. View the delivery result and response
✅ Local Development

Use tools like ngrok or localtunnel to expose your local server for webhook testing.

Webhook Delivery Logs

View webhook delivery history in the dashboard under Settings → Webhooks → Deliveries. Each delivery shows: