Skip to content

Developer docs

Webhooks

Subscribe an HTTPS endpoint to live events from your shop — appointments, invoices, payments, warranties. Each delivery is HMAC-SHA256 signed.

HTTPS onlyReal-time10-second timeout

Quick start

  1. Open Settings → Webhooks in the SalesThumb app.
  2. Click New subscription.
  3. Enter a label, your HTTPS URL, and pick the events you care about.
  4. Save the signing secret — it's shown once.
  5. Click Send test to verify your endpoint receives + verifies the request.

Payload envelope

Every event is wrapped in the same envelope. Thedata field varies by event type. For invoice.paid, invoiceId is always present;paidAmountCents is included on most paths, but depending on how the invoice was paid it may be the final payment or the invoice total. Fetch the invoice and its payments from the REST API for authoritative amounts.

{
  "event": "invoice.paid",
  "shopId": "01234567-89ab-cdef-0123-456789abcdef",
  "timestamp": "2026-04-26T14:23:11.000Z",
  "data": {
    "invoiceId": "01234567-...",
    "paidAmountCents": 84900
  }
}Language: json

Headers on every request:

Content-Type: application/json
User-Agent: SalesThumb-Webhook/1.0
X-SalesThumb-Event: invoice.paid
X-SalesThumb-Signature: sha256=<hex digest>
X-SalesThumb-Delivery-Id: <delivery uuid — the same on every retry>
X-SalesThumb-Delivery: <same value as X-SalesThumb-Delivery-Id>
X-SalesThumb-Attempt: <1 for the first try, up to 7>Language: text

Signature verification

Verify the signature before trusting a payload. The signature isHMAC-SHA256(secret, raw_body). Compare in constant time.

Node.js

import crypto from "node:crypto";

export function verifyWebhook(secret, header, rawBody) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  const provided = header.replace(/^sha256=/, "");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(provided, "hex"),
  );
}Language: javascript

Python

import hmac, hashlib

def verify_webhook(secret: str, header: str, raw_body: bytes) -> bool:
    expected = hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    provided = header.replace("sha256=", "", 1)
    return hmac.compare_digest(expected, provided)Language: python

Ruby

require "openssl"

def verify_webhook(secret, header, raw_body)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
  provided = header.sub(/^sha256=/, "")
  Rack::Utils.secure_compare(expected, provided)
endLanguage: ruby

Event catalog

All event types you can subscribe to. More are added as new modules ship — see /changelog for additions.

appointment.created

A new appointment was booked.

appointment.confirmed

An appointment moved to CONFIRMED status.

appointment.checked_in

Customer checked in for an appointment.

appointment.completed

An appointment was marked complete.

appointment.canceled

An appointment was canceled or no-showed.

quote.sent

A quote was sent to a customer.

quote.accepted

A customer accepted a quote.

invoice.created

A new invoice was created.

invoice.sent

An invoice was sent to a customer.

invoice.paid

An invoice was paid in full.

payment.received

A payment was recorded against an invoice.

payment.refunded

A payment was fully or partially refunded.

warranty.registered

A warranty was registered for an install.

warranty.claim_filed

A customer filed a warranty claim.

customer.created

A new customer record was created.

Retries & auto-pause

The platform considers a network error, a timeout, a redirect or any non-2xx response a failure. A failed delivery is retried automatically after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours: 7 attempts in all. Retries are picked up every few minutes, so the short gaps are approximate. After the 7th failed attempt the delivery is marked failed.

Every attempt of one delivery carries the same X-SalesThumb-Delivery-Id, so store it and skip any id you have already processed. Your endpoint should still be idempotent and queue work internally if it can't process synchronously.

If every delivery to a subscription has failed for more than 24 hours (a delivery used all 7 attempts with nothing delivered since its first try), the subscription is paused automatically and the shop owner (or the organization owner, for an organization webhook) gets one email. Settings → Webhooks shows why it was paused. Re-enable it from the dashboard once you've fixed the endpoint.

Timeout: 10 seconds. Long-running work should ack the webhook quickly (return 200 immediately) and process asynchronously.

Best practices

  • Verify X-SalesThumb-Signature on every request before trusting the payload.
  • Treat X-SalesThumb-Delivery-Id as an idempotency key — it stays the same across retries, so store it and skip duplicates.
  • Return 2xx fast. Queue heavy work for an async worker.
  • Subscribe to the narrowest set of events you need.
  • Rotate the signing secret periodically (90 days is reasonable).

FAQ

Is there a REST API?+

Yes — a read-only (GET) REST API covers customers, appointments, and invoices today; see the REST API docs for endpoints, scopes, and pagination. Use webhooks (push) for events you want to react to, the REST API (pull) for on-demand reads, and Zapier for tools that don't accept webhooks directly. CSV export covers bulk reporting. Email hello@salesthumb.com if you have a use case none of these cover and we'll prioritize accordingly.

Do you charge for webhook deliveries?+

No — webhook deliveries aren't billed on any plan. Two limits apply: each shop's event fan-out is capped at 1,000 events per hour (events over the cap are skipped, not queued), and a subscription is paused automatically once every delivery to it has failed for more than 24 hours.

Can I subscribe to events from multiple shops?+

Each subscription is scoped to a single shop. If you operate multiple shops on SalesThumb, create one subscription per shop or have your endpoint distinguish based on the shopId in the payload envelope.

How do I rotate the signing secret?+

From the subscription detail in the app. Rotation invalidates the old secret immediately, so deploy your secret update first, then rotate.

Is HMAC-SHA256 enough? Are you planning IP allowlisting?+

HMAC-SHA256 with a 32-byte secret is industry-standard for webhook authenticity (Stripe and GitHub both sign webhooks with it). IP allowlisting is on the roadmap for shops that want defense-in-depth.

Ready to wire it up?

Create your first subscription in 30 seconds.