Webhooks & Real-Time Sync
Webhooks allow external systems—such as ERPs, eCommerce storefronts (Shopify, WooCommerce, custom Next.js), CRM systems, and POS terminal management hubs—to receive instantaneous HTTP notifications whenever events happen in Tracepos.
Instead of continuously polling the API for inventory changes or completed sales, your application registers a webhook delivery URL and listens for real-time events.
1. Webhook Delivery & Security Headers
Tracepos signs every outbound webhook using HMAC-SHA256 with the secret key you configured when registering the endpoint.
Every delivery includes the following HTTP request headers:
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | Tracepos-Webhook/1.0 |
X-Tracepos-Signature | Hex-encoded HMAC-SHA256 hash of the raw JSON body using your secret key |
Authorization | Bearer <your_webhook_secret> |
X-Tracepos-Secret | Raw webhook secret token (for environments where Authorization headers are stripped) |
2. Cryptographic Signature Verification
Always verify the X-Tracepos-Signature header before trusting the payload. This guarantees the webhook originated from Tracepos and was not altered in transit.
:::: code-group ::: code-group-item Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
// Use express.raw() to capture the exact raw body string before JSON parsing
app.post('/api/webhooks/tracepos', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-tracepos-signature'];
const secret = process.env.TRACEPOS_WEBHOOK_SECRET;
if (!signature) {
return res.status(401).send('Missing signature');
}
// Calculate HMAC-SHA256
const computedHash = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
// Constant-time comparison to prevent timing attacks
const isValid = crypto.timingSafeEqual(
Buffer.from(signature, 'utf8'),
Buffer.from(computedHash, 'utf8')
);
if (!isValid) {
console.error('Invalid Tracepos webhook signature');
return res.status(403).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
console.log(`Received verified event: ${event.event_type}`);
// Process event asynchronously or push to worker queue (BullMQ/RabbitMQ)
handleEvent(event);
// Return fast 200 OK
res.status(200).json({ received: true });
});
function handleEvent(event) {
switch (event.event_type) {
case 'sales.created':
console.log('New sale completed:', event.data.invoice_number);
break;
case 'inventory.adjusted':
console.log('Stock changed for SKU:', event.data.sku, 'New stock:', event.data.current_stock);
break;
}
}
app.listen(3000, () => console.log('Webhook listener running on port 3000'));
::: ::: code-group-item Python (FastAPI / Flask)
import hmac
import hashlib
import os
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.getenv("TRACEPOS_WEBHOOK_SECRET").encode('utf-8')
@app.route('/api/webhooks/tracepos', methods=['POST'])
def receive_tracepos_webhook():
signature = request.headers.get('X-Tracepos-Signature')
if not signature:
abort(401, 'Missing signature')
# Get raw body bytes
raw_payload = request.get_data()
# Compute HMAC-SHA256
computed = hmac.new(WEBHOOK_SECRET, raw_payload, hashlib.sha256).hexdigest()
# Compare safely
if not hmac.compare_digest(signature, computed):
abort(403, 'Invalid signature')
event = request.get_json()
event_type = event.get('event_type')
data = event.get('data', {})
print(f"Verified event: {event_type} for outlet: {data.get('warehouse_slug')}")
# Process event
return jsonify({"received": True}), 200
if __name__ == '__main__':
app.run(port=5000)
::: ::: code-group-item PHP
<?php
// webhook_receiver.php
$secret = getenv('TRACEPOS_WEBHOOK_SECRET');
$signature = $_SERVER['HTTP_X_TRACEPOS_SIGNATURE'] ?? '';
$rawPayload = file_get_contents('php://input');
if (empty($signature) || empty($rawPayload)) {
http_response_code(401);
exit('Unauthorized: Missing signature or payload');
}
$computedHash = hash_hmac('sha256', $rawPayload, $secret);
if (!hash_equals($computedHash, $signature)) {
http_response_code(403);
exit('Forbidden: Invalid signature');
}
$event = json_decode($rawPayload, true);
$eventType = $event['event_type'] ?? 'unknown';
// Dispatch to background queue or database
file_put_contents('webhook.log', "[".date('Y-m-d H:i:s')."] Event: {$eventType}\n", FILE_APPEND);
http_response_code(200);
echo json_encode(['received' => true]);
::: ::::
3. Standardized Event Envelope
All Tracepos webhook dispatches follow a uniform JSON envelope:
{
"event_type": "string",
"data": {
"warehouse_slug": "string",
"...": "event_specific_fields"
}
}
event_type: Identifies the domain action (e.g.sales.created,product.updated).data.warehouse_slug: Identifies the physical branch or warehouse where the transaction or stock movement occurred. Crucial for multi-outlet retail routing.
4. Supported Event Types
Order & Transaction Events
Fired whenever orders are created, edited, or deleted in the POS or API:
sales.created: New customer sale finalized and paid.sales.updated: Order line items or tender breakdown edited.sales.deleted: Cancelled or voided order.purchases.created: Supplier procurement received and stock logged.stock_transfers.created: Inter-branch inventory transit dispatched.
Sample sales.created Payload
{
"event_type": "sales.created",
"data": {
"warehouse_slug": "vi-flagship",
"order_reference": "ord_9182ab3c4d5e",
"invoice_number": "SAL-00451",
"entity_name": "Adeola Adeleke",
"order_date": "2026-09-30 14:15:00",
"payment_status": "paid",
"totals": {
"subtotal": 5500.00,
"discount": 0.00,
"tax": 412.50,
"total": 5912.50,
"amount_paid": 5912.50,
"amount_due": 0.00
},
"items": [
{
"sku": "RICE-5KG-01",
"name": "Premium Basmati Rice 5kg",
"quantity": 2,
"unit_price": 2750.00,
"subtotal": 5500.00,
"discount_applied": 0.00
}
]
}
}
Product Master Catalog Events
Fired when items are added, edited, or retired in the master catalog:
product.created: New SKU added.product.updated: Retail price, name, or unit of measure changed.product.deleted: Product archived or removed.
Sample product.updated Payload
{
"event_type": "product.updated",
"data": {
"warehouse_slug": "vi-flagship",
"sku": "RICE-5KG-01",
"name": "Premium Basmati Rice 5kg",
"slug": "premium-basmati-rice-5kg",
"product_type": "single",
"category": "Grains & Cereals",
"brand": "Royal Feast",
"base_unit": "Bag",
"pricing": {
"purchase_price": 2200.00,
"sales_price": 2950.00,
"wholesale_price": 2700.00,
"mrp": 3000.00
},
"inventory": {
"is_serialized": false,
"alert_quantity": 10
},
"service_details": null,
"image_url": "https://cdn.tracepos.com/uploads/products/basmati.png"
}
}
Stock Adjustment Events
Fired when inventory counts change outside of orders (manual recounts, breakage, theft shrinkage, expiration write-offs):
inventory.adjusted: Warehouse stock level changed.
Sample inventory.adjusted Payload
{
"event_type": "inventory.adjusted",
"data": {
"warehouse_slug": "vi-flagship",
"sku": "RICE-5KG-01",
"name": "Premium Basmati Rice 5kg",
"adjustment_type": "subtract",
"quantity_adjusted": 2.00,
"current_stock": 42.00,
"reason": "Damaged goods in transit",
"date": "2026-09-30 15:30:00"
}
}
5. Reliability, Timeouts & Retries
- Timeout: Tracepos sets a 10-second HTTP connection/response timeout for webhook deliveries.
- Automatic Retries: If your server returns an HTTP status code outside of
2xx(e.g.500 Internal Server Error,502 Bad Gateway,504 Gateway Timeout), Tracepos retries delivery up to 3 times with exponential backoff. - Fast Acknowledgement: Your server must respond with an HTTP
200 OKstatus immediately upon receiving the payload. Defer heavy data processing, notifications, or third-party API calls to a background worker queue. - Idempotent Processing: Network retries may occasionally cause duplicate deliveries. Store the
order_reference,invoice_number, or event timestamp and check if you have already processed the event before executing side effects.
