Webhooks tell your software the moment something happens — a customer added, a visit moved, an invoice paid — by sending a signed POST to your URL.
Add an endpoint
In Settings → Developers → Webhooks: your https:// address and the events it wants. Its signing secret (whsec_…) is shown once. Apps can subscribe themselves through the API (POST /webhooks, scope webhooks). Send a test, read every delivery, resend one, roll the secret, or Edit the address, the events or the description from the same card — editing keeps the signing secret, so your receiver changes nothing. An app changes its own the same way (PATCH /webhooks/{id}); an endpoint an app made is the app's to change, and you can remove it.
Events
Customers (customer.created, .updated, .deleted), requests (request.created, .stage_changed, .deleted), quotes (quote.created, .sent, .accepted, .declined, .changes_requested), jobs (job.created, .status_changed, .completed, .cancelled, .deleted), visits (visit.scheduled, .rescheduled, .cancelled, .completed, .no_show, .deleted), invoices (invoice.sent, .updated, .paid, .voided) and payments (payment.succeeded, .failed, .refunded, .disputed, .dispute_closed). GET https://backend.sublake.com/api/public/v1/webhooks/events lists them all. An app is only ever told about records its scopes let it read.
What arrives
{"id": "…", "object": "event", "type": "invoice.paid",
"created_at": "2026-11-15T14:02:11Z", "business_id": "…",
"data": {"object": { …the invoice, as the API serves it… },
"changes": { …what changed… }}}Headers: Sublake-Signature, Sublake-Event-Id, Sublake-Event-Type, Sublake-Delivery-Id, Sublake-Attempt. The same event has the same id on every retry — use it to ignore a repeat.
Verify the signature
Sublake-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> with your secret. Refuse a request older than 5 minutes. While a rolled secret's overlap lasts, the header carries one v1 per secret — accept the request if any one matches.
import hmac, hashlib, time
def verify(secret, body, header):
parts = [p.split("=", 1) for p in header.split(",")]
t = int(next(v for k, v in parts if k == "t"))
if abs(time.time() - t) > 300:
return False
want = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return any(k == "v1" and hmac.compare_digest(want, v) for k, v in parts)Answer quickly
Answer 2xx within a few seconds and do the work afterwards. A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, then every 6 hours, for 3 days. An endpoint that fails for 3 days is turned off and the owner is told; an endpoint that answers 410 Gone is turned off at once.
Missed some? Read the events
GET https://backend.sublake.com/api/public/v1/events serves the same events, by the same ids, in the order they happened, for 90 days — the body your endpoint was sent, when one was sent. Read on with starting_after=<the last id you have> until has_more is false; GET /events/{id} reads one. An app whose endpoint was down catches up there without a resend.