# Webhooks & events

> SUBLAKE Docs · Developers · https://sublake.com/docs/webhooks

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.
