> For the complete documentation index, see [llms.txt](https://tsanet.gitbook.io/connect/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tsanet.gitbook.io/connect/api-reference/webhook-events.md).

# Webhook Events

What TSANet Connect sends to your endpoint, and how to verify it.

The **Webhooks V1** and **Webhooks V2** pages cover how to *subscribe*. This page covers what Connect actually **delivers** to your endpoint once you have.

## Payload versions

| Version         | Subscribe with      | Events                                          | Format                     |
| --------------- | ------------------- | ----------------------------------------------- | -------------------------- |
| **V2**          | `POST /v2/webhooks` | All 5 event types                               | CloudEvents 1.0 structured |
| **V1** (legacy) | `POST /v1/webhooks` | `collaboration-request.created`, `note.created` | Flat JSON                  |

{% hint style="warning" %}
V1 is frozen — no new events or fields will ever be added to it, and it is scheduled for removal after **2027-01-01**. Use `POST /v2/webhooks` for new integrations.
{% endhint %}

## Event types (V2)

| CloudEvent `type`                                           | Fired when                                                                      | `data` fields                |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------- |
| `org.tsanet.connect.collaboration-request.created`          | A collaboration request is submitted                                            | `requestToken`               |
| `org.tsanet.connect.collaboration-request.closed`           | A collaboration request is closed                                               | `requestToken`               |
| `org.tsanet.connect.collaboration-request.note.created`     | A note is added                                                                 | `requestToken`, `noteToken`  |
| `org.tsanet.connect.collaboration-request.response.created` | An approval, rejection, information request, or information response is created | `requestToken`, `responseId` |
| `org.tsanet.connect.collaboration-request.response.updated` | An existing response is revised                                                 | `requestToken`, `responseId` |

Payloads are deliberately thin. Use the tokens to fetch the full record from the REST API — for a response, `responseId` tells you which record to fetch to determine its type and status.

## Delivery headers

| Header                | Value                                                           |
| --------------------- | --------------------------------------------------------------- |
| `Content-Type`        | `application/cloudevents+json` (V2) or `application/json` (V1)  |
| `X-Hub-Signature-256` | `sha256=<64 hex chars>` — HMAC of the raw body                  |
| `X-Connect-Delivery`  | The CloudEvent `id`. Use as your idempotency key.               |
| `X-Connect-Event`     | The CloudEvent `type`. Lets you route without parsing the body. |

`X-Connect-Delivery` and `X-Connect-Event` mirror the `id` and `type` fields in the body, so you can route and deduplicate at the HTTP layer.

## Verifying signatures

Every delivery is signed with HMAC-SHA256 over the **raw request body**, using your subscription secret. Always verify before processing.

{% code title="Node.js" %}

```javascript
const crypto = require('crypto');

function verify(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)          // the raw bytes, before any JSON parsing
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}
```

{% endcode %}

{% hint style="danger" %}
Compute the HMAC over the raw body bytes. Re-serialising the parsed JSON changes the byte sequence and the signature will not match. Compare with a constant-time function, never `===`.
{% endhint %}

Your secret is returned **once**, when the subscription is created. Rotate it with `POST /v1/webhooks/{id}/secret` — the new secret is likewise returned only once. Allow a short overlap before retiring the old one.

## Delivery guarantees

* **At-least-once.** Your endpoint may receive the same event more than once. Deduplicate on the CloudEvent `id` (V2) or on `requestToken` + `eventType` + `timestamp` (V1). The `id` is stable across redeliveries.
* **Timeout.** Respond within **30 seconds**. Acknowledge first, process afterwards.
* **Retries.** `5xx` responses and network timeouts trigger redelivery. **Two retries are attempted, with a 1-hour delay between each.** `4xx` responses are treated as permanent failures and are not retried.
* **Dead-letter.** Deliveries that exhaust every retry are moved to a dead-letter queue and flagged in the delivery log. Contact TSANet Support to replay them.

Inspect delivery history with `GET /v1/webhooks/{id}/deliveries`.

## Subscription filters

A subscription can narrow what it receives by `eventTypes`, `caseDirections`, and `partnerCompanyIds`. Omitting a filter means it is not applied.

Directions are relative to the company that owns the subscription:

* `INBOUND` — your company **receives** the collaboration request.
* `OUTBOUND` — your company **submits** it.

Event type strings must match the CloudEvent `type` exactly as delivered, for example `org.tsanet.connect.collaboration-request.created`.

## Source identifier

The CloudEvent `source` field identifies the environment that sent the event:

| Environment | `source`                      |
| ----------- | ----------------------------- |
| Production  | `https://connect2.tsanet.us`  |
| Beta        | `https://connect2.tsanet.net` |

The `subscriptionid` field carries the ID of the subscription the event was delivered to — useful when several subscriptions point at the same endpoint.
