Help article

Webhooks – Setup and Technical Guide

Webhooks let SalesBinder notify another application whenever selected events occur, such as an invoice being created, an estimate being converted, or an inventory item being updated.

Updated August 27th, 2026

Webhooks

Webhooks let SalesBinder notify another application whenever selected events occur, such as an invoice being created, an estimate being converted, or an inventory item being updated.

Unlike regularly checking the SalesBinder API for changes, webhooks send the event to your application automatically and securely.

Setting Up a Webhook

To create a webhook:

  1. Open Settings → Webhooks in SalesBinder.
  2. Select Add Webhook.
  3. Enter a descriptive name and your HTTPS endpoint URL.
  4. Select the events you want to receive.
  5. Select Create Webhook.
  6. Copy and securely save the signing secret that appears.
  7. Enable the webhook and use Send Test to confirm your endpoint is working.

New webhooks start disabled so you can finish configuring your receiver before live events are delivered. Each account can configure up to 20 webhook endpoints.

For security, endpoint URLs must use HTTPS and resolve to a publicly accessible address.

How Webhook Delivery Works

When a selected event occurs, SalesBinder sends an HTTP POST request containing a JSON payload similar to:

{
  "id": "c701bfcd-d86f-4379-8502-ff5c4a821521",
  "type": "invoice.created",
  "api_version": "v3",
  "created_at": "2026-08-27T15:58:12+00:00",
  "account_id": "4d76bc2e-b198-4bd6-95f5-7439b86a92d9",
  "data": {
    "object": {
      "id": "445403e5-e772-4074-aa69-67d25b3daa96"
    }
  }
}

The request also includes these helpful headers:

  • X-SalesBinder-Event — the event type.
  • X-SalesBinder-Delivery — the unique delivery identifier.
  • X-SalesBinder-Signature — the timestamped HMAC signature.
  • X-SalesBinder-Webhook-Version — the webhook API version.

Your endpoint should return an HTTP 2xx response promptly. We recommend saving the event to a queue and processing it in the background instead of performing lengthy work before responding.

Webhook processing should also be idempotent. Occasionally, the same event may be delivered more than once, so use the event’s id to prevent duplicate processing.

Verifying the Signing Secret

Every webhook request is signed using HMAC-SHA256. Always verify the signature before trusting or processing its contents.

The signature header looks like:

t=1787846292,v3=generated_signature

To verify it:

  1. Read the request body exactly as received.
  2. Extract t and all v3 values from the signature header.
  3. Combine the timestamp, a period, and the raw request body:
    timestamp.raw_request_body
  4. Calculate an HMAC-SHA256 using your signing secret.
  5. Compare your result with each v3 value using a timing-safe comparison.
  6. Reject requests with an invalid signature or an unexpectedly old timestamp.

Do not decode and re-encode the JSON before verifying it, because even minor formatting differences will change the signature.

JavaScript (Node.js) Example

import crypto from 'node:crypto';
import express from 'express';

const app = express();

app.post(
  '/webhooks/salesbinder',
  express.raw({ type: 'application/json' }),
  (request, response) => {
    const header = request.get('X-SalesBinder-Signature') ?? '';
    const timestamp = header.match(/t=(\d+)/)?.[1];
    const signatures = [...header.matchAll(/v3=([a-f0-9]{64})/gi)]
      .map((match) => match[1]);

    if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return response.sendStatus(401);
    }

    const expected = crypto
      .createHmac('sha256', process.env.SALESBINDER_WEBHOOK_SECRET)
      .update(`${timestamp}.${request.body.toString('utf8')}`)
      .digest('hex');

    const verified = signatures.some((signature) =>
      crypto.timingSafeEqual(
        Buffer.from(expected, 'hex'),
        Buffer.from(signature, 'hex'),
      )
    );

    if (!verified) {
      return response.sendStatus(401);
    }

    const event = JSON.parse(request.body.toString('utf8'));

    console.log(`Received ${event.type}`, event.data.object);

    return response.sendStatus(200);
  },
);

app.listen(3000);

PHP Example

<?php

$secret = 'your_webhook_signing_secret';
$rawBody = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_SALESBINDER_SIGNATURE'] ?? '';

$timestamp = null;
$signatures = [];

foreach (explode(',', $signatureHeader) as $part) {
    [$key, $value] = array_pad(explode('=', trim($part), 2), 2, null);

    if ($key === 't') {
        $timestamp = $value;
    } elseif ($key === 'v3' && $value !== null) {
        $signatures[] = $value;
    }
}

if ($timestamp === null || abs(time() - (int)$timestamp) > 300) {
    http_response_code(401);
    exit('Invalid webhook timestamp');
}

$expected = hash_hmac(
    'sha256',
    $timestamp . '.' . $rawBody,
    $secret
);

$verified = false;

foreach ($signatures as $signature) {
    if (hash_equals($expected, $signature)) {
        $verified = true;
        break;
    }
}

if (!$verified) {
    http_response_code(401);
    exit('Invalid webhook signature');
}

$event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);

// Save or queue the event for processing.

http_response_code(200);

Keep signing secrets private and never include them in logs, source control, or client-side code.

When you rotate a signing secret, the previous secret remains valid for 24 hours. During that period, requests contain signatures for both secrets, giving you time to update your application safely.

Retries and Error Handling

SalesBinder considers any 2xx response successful.

Temporary failures are retried when your endpoint returns:

  • 408 Request Timeout
  • 425 Too Early
  • 429 Too Many Requests
  • Any 5xx response
  • A network or connection error

SalesBinder makes up to eight delivery attempts using progressively longer delays: approximately 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, and 24 hours.

Redirects and most other 4xx responses are treated as permanent failures and are not automatically retried. Webhook requests have a 5-second connection timeout and a 10-second overall request timeout.

After 10 consecutive terminal delivery failures, the endpoint is automatically disabled. Once the underlying issue is corrected, you can re-enable it from the Webhooks settings area.

Reviewing Delivery History

Each webhook includes a Delivery History link that opens the Integrations Log filtered to that endpoint.

The log shows queued, successful, retrying, and failed deliveries, along with HTTP status codes and diagnostic details. Failed deliveries can be manually sent again using Retry Delivery.

Webhook delivery history is retained for 14 days.

Recommended Practices

  • Verify every request before processing it.
  • Respond with 2xx quickly and perform longer work asynchronously.
  • Make event processing idempotent.
  • Return 429 or 5xx only when retrying later may succeed.
  • Store signing secrets securely and rotate them if they may have been exposed.
  • Monitor Delivery History after enabling a new endpoint.
Network Status:
100% Global Availability