Webhook Integration Guide
Build real-time integrations with Stockaj using webhooks. This guide covers setup, payload handling, security verification, and common patterns.
How It Works
- You register a webhook URL in Stockaj
- When events occur (e.g., rental created), Stockaj sends an HTTP POST to your URL
- Your server processes the payload and responds with a 2xx status
Event occurs → Stockaj creates payload → POST to your URL → You process it
Setting Up a Webhook Receiver
Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
// Important: use raw body for signature verification
app.post('/webhooks/stockaj', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-stockaj-signature'];
const event = req.headers['x-stockaj-event'];
const secret = process.env.STOCKAJ_WEBHOOK_SECRET;
// Verify signature
const computed = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computed))) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body);
// Handle the event
switch (event) {
case 'rent.created':
console.log('New rental:', payload.data.id);
// Your logic here
break;
case 'item.low_stock':
console.log('Low stock alert:', payload.data.name);
// Your logic here
break;
default:
console.log(`Unhandled event: ${event}`);
}
res.status(200).json({ received: true });
});
app.listen(3000);
Python (Flask)
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['STOCKAJ_WEBHOOK_SECRET']
@app.route('/webhooks/stockaj', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Stockaj-Signature')
event = request.headers.get('X-Stockaj-Event')
# Verify signature
computed = hmac.new(
WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(computed, signature):
return jsonify({'error': 'Invalid signature'}), 401
payload = request.get_json()
# Handle events
if event == 'rent.created':
handle_new_rental(payload['data'])
elif event == 'rent.status_changed':
handle_status_change(payload['data'])
elif event == 'item.low_stock':
handle_low_stock(payload['data'])
return jsonify({'received': True}), 200
PHP (Laravel)
Route::post('/webhooks/stockaj', function (Request $request) {
$signature = $request->header('X-Stockaj-Signature');
$secret = config('services.stockaj.webhook_secret');
$computed = hash_hmac('sha256', $request->getContent(), $secret);
if (!hash_equals($computed, $signature)) {
abort(401, 'Invalid signature');
}
$event = $request->header('X-Stockaj-Event');
$payload = $request->all();
match ($event) {
'rent.created' => dispatch(new HandleNewRental($payload['data'])),
'rent.status_changed' => dispatch(new HandleStatusChange($payload['data'])),
'item.low_stock' => dispatch(new HandleLowStock($payload['data'])),
default => Log::info("Unhandled webhook event: {$event}"),
};
return response()->json(['received' => true]);
});
Common Integration Patterns
Slack Notifications
Send rental notifications to a Slack channel:
case 'rent.created':
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `📋 New rental created by ${payload.data.renter.name}`,
blocks: [{
type: 'section',
text: {
type: 'mrkdwn',
text: `*New Rental #${payload.data.id}*\n` +
`Renter: ${payload.data.renter.name}\n` +
`Items: ${payload.data.items.length}\n` +
`Status: ${payload.data.status}`
}
}]
})
});
break;
Inventory Sync
Keep an external database in sync:
case 'item.updated':
await externalDb.items.update({
where: { stockaj_id: payload.data.id },
data: {
name: payload.data.name,
quantity: payload.data.quantity,
updated_at: payload.timestamp,
}
});
break;
Low Stock Alerts
Trigger automated reordering:
case 'item.low_stock':
const item = payload.data;
if (item.quantity <= item.minimum_quantity) {
await purchaseOrderSystem.createOrder({
item_sku: item.external_id,
quantity: item.minimum_quantity * 2 - item.quantity,
notes: `Auto-reorder from Stockaj low stock alert`,
});
}
break;
Best Practices
- Respond quickly — Return a 2xx response within the timeout period (default: 10s). Process heavy work asynchronously.
- Be idempotent — Use the delivery ID (
X-Stockaj-Delivery) to detect and skip duplicate deliveries. - Always verify signatures — Never trust a webhook payload without verifying the HMAC signature.
- Handle failures gracefully — Log errors and alert your team if webhook processing fails.
- Use HTTPS — Your webhook URL must use HTTPS in production.
- Monitor deliveries — Check the delivery log in Stockaj to identify failed deliveries.
Troubleshooting
Webhook not receiving events
- Check the webhook is active in Settings → Webhooks
- Verify the URL is publicly accessible (not localhost)
- Check the events list includes the events you expect
- Use Send Test to send a test payload
Signature verification failing
- Ensure you're using the raw request body (not parsed JSON) for hashing
- Check you're using
HMAC-SHA256(not SHA-1 or plain SHA-256) - Verify the secret matches exactly (no trailing spaces)
- Use timing-safe comparison to prevent timing attacks
Request timing out
- Increase the webhook timeout (up to 30 seconds)
- Move processing to a background queue
- Return
200 OKimmediately, then process asynchronously