# Webhooks

> Receive signed events when records change, and verify each delivery.

- Canonical page: https://synconone.com/docs/webhooks
- Section: Developers
- Updated: 2026-10-22

Webhooks send an HTTPS request to your endpoint when something changes in the workspace. They are included on Pro and Enterprise.

## Add an endpoint

1. Open Settings → Webhooks and choose **Add an endpoint**.
2. Enter your HTTPS address and choose the events to send.
3. Copy the signing secret. You need it to verify deliveries.

## Events

| Event | Sent when |
| --- | --- |
| `record.created` | A record was created. |
| `record.updated` | A record was changed. |
| `record.deleted` | A record was permanently deleted. |
| `opportunity.stage_changed` | An opportunity moved stage. |
| `opportunity.won` | An opportunity moved to a won stage. |
| `opportunity.lost` | An opportunity moved to a lost stage. |
| `task.completed` | A task was marked done. |
| `email.received` | An email arrived in a connected mailbox. |
| `meeting.analyzed` | A recorded meeting finished analysis. |
| `forecast.snapshot_created` | A forecast snapshot was taken. |

Each delivery carries the event name in `x-syncon-event` and a delivery id in `x-syncon-delivery`. A failed delivery is retried with growing gaps, up to eight attempts.

## Verify a delivery

The `x-syncon-signature` header looks like `t=1729590000,v1=…`: a Unix timestamp and an HMAC-SHA256, in hex, of the timestamp, a full stop and the raw request body. Reject a delivery whose timestamp is more than five minutes old or whose signature does not match.

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

// body: the raw request body as a string, before any JSON parsing
export function isGenuine(body, header, secret) {
  const parts = Object.fromEntries(
    (header ?? "").split(",").map((part) => {
      const at = part.indexOf("=");
      return [part.slice(0, at).trim(), part.slice(at + 1).trim()];
    }),
  );
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || !/^[0-9a-f]{64}$/.test(parts.v1 ?? "")) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest();
  return timingSafeEqual(expected, Buffer.from(parts.v1, "hex"));
}
```

> **Verify against the raw body** Parsing the JSON and serialising it again changes the bytes, and the signature will not match. Read the body as text first.
