Skip to main content

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

  1. You register a webhook URL in Stockaj
  2. When events occur (e.g., rental created), Stockaj sends an HTTP POST to your URL
  3. 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

  1. Respond quickly — Return a 2xx response within the timeout period (default: 10s). Process heavy work asynchronously.
  2. Be idempotent — Use the delivery ID (X-Stockaj-Delivery) to detect and skip duplicate deliveries.
  3. Always verify signatures — Never trust a webhook payload without verifying the HMAC signature.
  4. Handle failures gracefully — Log errors and alert your team if webhook processing fails.
  5. Use HTTPS — Your webhook URL must use HTTPS in production.
  6. Monitor deliveries — Check the delivery log in Stockaj to identify failed deliveries.

Troubleshooting

Webhook not receiving events

  1. Check the webhook is active in Settings → Webhooks
  2. Verify the URL is publicly accessible (not localhost)
  3. Check the events list includes the events you expect
  4. Use Send Test to send a test payload

Signature verification failing

  1. Ensure you're using the raw request body (not parsed JSON) for hashing
  2. Check you're using HMAC-SHA256 (not SHA-1 or plain SHA-256)
  3. Verify the secret matches exactly (no trailing spaces)
  4. Use timing-safe comparison to prevent timing attacks

Request timing out

  1. Increase the webhook timeout (up to 30 seconds)
  2. Move processing to a background queue
  3. Return 200 OK immediately, then process asynchronously