Tracepos Developer HubTracepos Developer Hub
Home
Guide
POS & Integrations
API Reference
Webhooks
Home
Guide
POS & Integrations
API Reference
Webhooks
  • Core Catalog & Outlets

    • Products API
    • Categories & Brands API
    • Warehouses & Outlets API
  • Parties & CRM

    • Customers & Suppliers API
  • Operations & Stock

    • Sales & Transactions API
    • Purchases & Procurement API
    • Inventory & Price Adjustments API
    • Returns & Reversals API
  • Financial Ledger & Expenses

    • Payments & Settlements API
    • Expense Management API
    • Double-Entry Accounting Architecture
  • Real-Time Sync

    • Webhooks & Real-Time Sync

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:

HeaderDescription
Content-Typeapplication/json
User-AgentTracepos-Webhook/1.0
X-Tracepos-SignatureHex-encoded HMAC-SHA256 hash of the raw JSON body using your secret key
AuthorizationBearer <your_webhook_secret>
X-Tracepos-SecretRaw 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 OK status 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.