Developer overview
Events & automation16 minute readAPI v1

Signed sales webhooks

Receive order, inventory and recurring-order events with HMAC signatures, stable delivery IDs and bounded retries.

Browse developer docs

Webhooks push real-time event notifications to your system as HTTP POST requests. Instead of polling the API for changes, your system receives events as they happen.

Setting Up a Webhook

  1. Go to Settings > Webhooks in the SalesPro Hub dashboard.
  2. Click Add Endpoint.
  3. Enter:
    • Name — A label for this endpoint (e.g., "Sage Order Sync").
    • Endpoint URL — The HTTPS URL on your server that will receive webhook POST requests.
    • Events — Select which events to subscribe to.
  4. Click Create.
  5. Copy the signing secret immediately — it starts with whsec_ and is only shown once.

Available Events

Order Events

EventFires When
order.submittedA sales rep submits an order (draft → pending)
order.confirmedAn admin confirms an order (pending → confirmed)
order.deliveredAn order is marked as delivered
order.cancelledAn order is cancelled
order.signedA customer or representative adds an order signature

Inventory Events

EventFires When
inventory.low_stockA product's stock drops below its threshold
inventory.out_of_stockA product's stock reaches zero
inventory.adjustedAn admin manually adjusts stock
inventory.syncedStock is updated via the Inventory API

Recurring Order Events

EventFires When
recurring_order.createdA recurring order schedule is created
recurring_order.executedA recurring schedule creates an order

Webhook Delivery

When an event fires, SalesPro Hub sends an HTTP POST request to your endpoint URL.

Request Headers

HeaderDescription
Content-Typeapplication/json
X-Webhook-EventEvent type (e.g., order.submitted)
X-Webhook-IdStable event-delivery identifier; use it as an idempotency key across retries
X-Webhook-AttemptDelivery attempt number, beginning at 1
X-Webhook-SignatureHMAC-SHA256 signature for verification
X-Webhook-TimestampUnix timestamp when the webhook was sent
User-AgentSalesProHub-Webhook/1.0

Request Body

The body is a JSON object containing the event type, timestamp, and event-specific data.

Payload Examples

order.submitted

json
{
  "event": "order.submitted",
  "timestamp": "2026-02-09T08:30:00+02:00",
  "data": {
    "order_number": "ORD-20260209-0001",
    "status": "pending",
    "type": "order",
    "order_date": "2026-02-09",
    "delivery_date": "2026-02-16",
    "customer": {
      "name": "ABC Trading (Pty) Ltd",
      "contact_person": "John Smith",
      "email": "john@abctrading.co.za"
    },
    "sales_rep": {
      "name": "Jane Doe"
    },
    "subtotal": "10000.00",
    "vat_rate": "15.00",
    "vat_amount": "1500.00",
    "total": "11500.00",
    "delivery_address": "123 Main Road, Sandton, 2196",
    "notes": "Deliver before 10am",
    "items": [
      {
        "sku": "WDG-001",
        "name": "Widget A",
        "quantity": 50,
        "unit_price": "200.00",
        "discount_percent": "0.00",
        "line_total": "10000.00"
      }
    ]
  }
}

order.confirmed / order.delivered / order.cancelled

Same structure as order.submitted with the status field reflecting the new status.

inventory.low_stock

json
{
  "event": "inventory.low_stock",
  "timestamp": "2026-02-09T14:00:00+02:00",
  "data": {
    "sku": "WDG-002",
    "name": "Widget B",
    "stock_qty": 8,
    "reserved_qty": 3,
    "available_qty": 5,
    "low_stock_threshold": 10,
    "previous_qty": 15,
    "is_low_stock": true,
    "is_out_of_stock": false,
    "movement_type": "deduction",
    "related_order": "ORD-20260209-0003"
  }
}

inventory.out_of_stock

Same structure as inventory.low_stock with is_out_of_stock: true and stock_qty: 0.

inventory.adjusted

json
{
  "event": "inventory.adjusted",
  "timestamp": "2026-02-09T09:00:00+02:00",
  "data": {
    "sku": "WDG-001",
    "name": "Widget A",
    "stock_qty": 200,
    "reserved_qty": 20,
    "available_qty": 180,
    "low_stock_threshold": 25,
    "previous_qty": 150,
    "is_low_stock": false,
    "is_out_of_stock": false,
    "movement_type": "adjustment",
    "related_order": null
  }
}

inventory.synced

Same structure as inventory.adjusted with movement_type: "sync". Fires when stock is updated through the Inventory API.


Signature Verification

Every webhook includes an HMAC-SHA256 signature so you can verify it came from SalesPro Hub and wasn't tampered with.

How Signing Works

  1. SalesPro Hub creates a signed payload: {timestamp}.{json_body}
  2. Signs it with your endpoint's secret using HMAC-SHA256.
  3. Sends the signature in the X-Webhook-Signature header and the timestamp in X-Webhook-Timestamp.

Verification Steps

  1. Read the X-Webhook-Timestamp and X-Webhook-Signature headers.
  2. Get the raw request body (before any JSON parsing).
  3. Create the signed payload string: {timestamp}.{raw_body}.
  4. Compute HMAC-SHA256 using your webhook secret.
  5. Compare your computed signature with the received signature.
  6. Optionally verify the timestamp is within 5 minutes to prevent replay attacks.

PHP Example

php
function verifyWebhookSignature(string $payload, string $signature, string $timestamp, string $secret): bool
{
    $signedPayload = "{$timestamp}.{$payload}";
    $expectedSignature = hash_hmac('sha256', $signedPayload, $secret);

    if (!hash_equals($expectedSignature, $signature)) {
        return false;
    }

    // Optional: reject webhooks older than 5 minutes
    if (abs(time() - (int) $timestamp) > 300) {
        return false;
    }

    return true;
}

// Usage in your endpoint:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$secret = 'whsec_your_secret_here';

if (!verifyWebhookSignature($payload, $signature, $timestamp, $secret)) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($payload, true);
// Process the event...

Python Example

python
import hmac
import hashlib
import time
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = 'whsec_your_secret_here'

@app.route('/webhooks/salesrep', methods=['POST'])
def handle_webhook():
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-Webhook-Signature', '')
    timestamp = request.headers.get('X-Webhook-Timestamp', '')

    # Verify signature
    signed_payload = f"{timestamp}.{payload}"
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        abort(401, 'Invalid signature')

    # Optional: check timestamp freshness
    if abs(time.time() - int(timestamp)) > 300:
        abort(401, 'Timestamp too old')

    event = request.get_json()
    event_type = event['event']

    if event_type == 'order.submitted':
        # Process new order...
        pass
    elif event_type == 'inventory.low_stock':
        # Handle low stock alert...
        pass

    return '', 200

Node.js Example

javascript
const crypto = require('crypto');
const express = require('express');
const app = express();

const WEBHOOK_SECRET = 'whsec_your_secret_here';

app.post('/webhooks/salesrep', express.raw({ type: 'application/json' }), (req, res) => {
    const payload = req.body.toString();
    const signature = req.headers['x-webhook-signature'];
    const timestamp = req.headers['x-webhook-timestamp'];

    // Verify signature
    const signedPayload = `${timestamp}.${payload}`;
    const expected = crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(signedPayload)
        .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
        return res.status(401).send('Invalid signature');
    }

    // Optional: check timestamp freshness (5 minutes)
    if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
        return res.status(401).send('Timestamp too old');
    }

    const event = JSON.parse(payload);
    console.log(`Received event: ${event.event}`);

    // Process the event...

    res.status(200).send('OK');
});

C# Example

csharp
using System.Security.Cryptography;
using System.Text;

public bool VerifySignature(string payload, string signature, string timestamp, string secret)
{
    var signedPayload = $"{timestamp}.{payload}";
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload));
    var expected = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();

    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(signature)
    );
}

Retry Policy

If your endpoint doesn't respond with a 2xx status code, SalesPro Hub retries the delivery:

AttemptDelay
1st retry10 seconds
2nd retry30 seconds
3rd retry2 minutes
4th retry5 minutes
5th retry15 minutes

After 5 failed attempts, the delivery is marked as permanently failed.

Auto-Disable

If an endpoint accumulates 10 consecutive failures (across any deliveries), it is automatically disabled. You'll receive an in-app notification when this happens.

To re-enable: go to Settings > Webhooks, click the toggle to enable the endpoint. This resets the failure counter.

Testing Webhooks

Click the Test button next to any endpoint to send a test webhook:

json
{
  "event": "webhook.test",
  "timestamp": "2026-02-09T12:00:00+02:00",
  "data": {
    "message": "This is a test webhook delivery from SalesPro Hub.",
    "endpoint_name": "Sage Order Sync"
  }
}

Delivery History

Click View Deliveries on any endpoint to see:

  • Event type
  • Delivery status (success, failed, pending)
  • HTTP response code
  • Attempt number
  • Error message (if failed)
  • Timestamps

You can retry any failed delivery from this page.

Managing Endpoints

Disable/Enable

Toggle an endpoint to temporarily stop receiving webhooks without deleting it. Re-enabling resets the failure counter.

Delete

Permanently removes the endpoint and all its delivery history. Any pending retries are cancelled.

Best Practices

  1. Respond quickly — Return a 200 OK as soon as you receive the webhook. Process the event asynchronously if needed.
  2. Handle duplicates — In rare cases, you may receive the same event twice. Use the order_number or sku + timestamp to deduplicate.
  3. Verify signatures — Always validate the HMAC signature in production.
  4. Use HTTPS — Webhook payloads contain business data. Only use HTTPS endpoint URLs.
  5. Monitor delivery history — Check for failures regularly and investigate any patterns.
  6. Keep your secret safe — Treat the signing secret like a password.

Build against the real API

Professional API features are included in the 14-day trial.