🔔 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:
- Go to Settings → Webhooks in your dashboard
- Click Add Webhook Endpoint
- Enter your endpoint URL (must be HTTPS in production)
- Select the events you want to receive
- Copy and save the webhook secret for signature verification
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)
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 });
});
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) |
{
"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 |
{
"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
Respond with a 2xx status code within
30 seconds to acknowledge receipt. Process the webhook
asynchronously if needed.
Response Requirements
-
Return
200 OKor202 Acceptedto acknowledge -
Response body is optional but can be
{"received": true} - Respond within 30 seconds to avoid timeout
- Non-2xx responses trigger automatic retries
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:
- Go to Settings → Webhooks
- Click the Test button next to your endpoint
- Select an event type to send
- View the delivery result and response
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:
- Event type and timestamp
- Request payload sent
- Response status and body
- Retry attempts and status