> ## Documentation Index
> Fetch the complete documentation index at: https://docs.turrisfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks Overview

> Receive automated notifications for compliance events

## What are Webhooks?

Webhooks allow you to receive automated notifications when important compliance events occur for your downstream entities and agents. Instead of continuously polling our API for updates, we'll automatically send HTTP POST requests to your specified endpoint whenever license statuses change or NIPR data is updated.

## Quick Start

<Steps>
  <Step title="Set up your endpoint">Create an endpoint to receive POST requests</Step>
  <Step title="Register your webhook URL">Register your webhook URL using our API</Step>
  <Step title="Verify signatures">Verify webhook signatures using your client secret</Step>
  <Step title="Handle events">Process events based on the payload data</Step>
  <Step title="Test integration">Test your integration using our test webhook feature</Step>
</Steps>

## Available Webhook Events

<CardGroup cols={2}>
  <Card title="Agent Compliance Status Change" icon="user-shield" href="/guides/webhooks/agent-compliance-status-change">
    Triggered when an agent's compliance status changes due to license, appointment, or requirement updates.
  </Card>

  <Card title="Agency Compliance Status Change" icon="building-shield" href="/guides/webhooks/downstream-entity-compliance-status-change">
    Triggered when a downstream entity's (agency) compliance status changes due to license, appointment, or requirement
    updates.
  </Card>

  <Card title="Compliance Data Synchronized" icon="rotate" href="/guides/webhooks/compliance-data-synchronized">
    Triggered when NIPR data synchronization completes for an agent or downstream entity.
  </Card>

  <Card title="Producer Agreement Executed" icon="file-signature" href="/guides/webhooks/producer-agreement-executed">
    Triggered when a downstream entity and your upstream entity have fully executed a producer agreement.
  </Card>
</CardGroup>

### Planned Webhooks

We plan to build many more webhook events, including:

* Downstream entity onboarding
* Downstream entity/agent authority status
* E\&O compliance status changes
* E\&O/Cyber policy renewals

## Payload Envelope

All webhook payloads follow this general structure:

```json theme={null}
{
  "webhookType": "WEBHOOK_TYPE",
  "upstreamEntityId": "507f1f77bcf86cd799439010",
  "payload": { ... }
}
```

| Field              | Description                                                      |
| ------------------ | ---------------------------------------------------------------- |
| `webhookType`      | The event type (e.g., `AGENT_COMPLIANCE_STATUS_CHANGE`)          |
| `upstreamEntityId` | Your upstream entity ID                                          |
| `payload`          | Event-specific data — see each event's documentation for details |

## Setting Up Your Webhook Endpoint

Your webhook endpoint must:

<Check>Accept POST requests with JSON payloads</Check>
<Check>Respond with 2xx status codes (200-299) for successful processing</Check>
<Check>Respond within 30 seconds to avoid timeout</Check>
<Check>Handle duplicate events gracefully (use payload content for deduplication)</Check>
<Check>Verify the `X-Turris-Signature` header using your client secret</Check>

### Example Endpoint Response

```json theme={null}
{
  "status": "received",
  "processedAt": "2024-01-15T14:22:05Z"
}
```

## Webhook Security

All webhook requests are signed using **HMAC-SHA256** with your client secret. This allows you to verify that the request originated from Turris and that the payload has not been tampered with.

Webhook requests include the following headers:

| Header               | Description                                          |
| -------------------- | ---------------------------------------------------- |
| `Content-Type`       | `application/json`                                   |
| `X-Turris-Signature` | HMAC-SHA256 hex signature of the payload             |
| `X-Turris-Timestamp` | Unix timestamp (seconds) when the request was signed |

The request body is sent as **plaintext JSON** — no decryption is required.

<Warning>
  Your client secret is the only way to verify webhook authenticity. Store it securely as an environment variable —
  never hard-code it in your application.
</Warning>

## Verifying Webhook Signatures

To verify a webhook request is authentic, recompute the HMAC-SHA256 signature and compare it to the `X-Turris-Signature` header.

### How the Signature is Computed

The signature is computed over the string `{timestamp}.{payload}`, where:

* `{timestamp}` is the value of the `X-Turris-Timestamp` header
* `{payload}` is the raw JSON request body (as a string)

This binds the timestamp to the payload, preventing replay attacks.

### Verification Example (Node.js)

```javascript theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyWebhookSignature(rawBody, signature, timestamp, hexSecret) {
  const signedContent = `${timestamp}.${rawBody}`;
  const expectedSignature = createHmac('sha256', Buffer.from(hexSecret, 'hex')).update(signedContent).digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  return timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
}
```

### Usage Example

```javascript theme={null}
// Example: Express.js webhook endpoint
// Use the verify callback to capture the raw body buffer before JSON parsing
app.post(
  '/webhook',
  express.json({
    verify: (req, _res, buf) => {
      req.rawBody = buf.toString('utf8');
    },
  }),
  (req, res) => {
    const signature = req.headers['x-turris-signature'];
    const timestamp = req.headers['x-turris-timestamp'];

    const isValid = verifyWebhookSignature(req.rawBody, signature, timestamp, process.env.TURRIS_CLIENT_SECRET);

    if (!isValid) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    console.log('Received webhook:', req.body.webhookType);
    console.log('Payload:', req.body.payload);

    res.status(200).json({ status: 'received' });
  },
);
```

<Tip>
  To protect against replay attacks, you can also verify that the `X-Turris-Timestamp` is within an acceptable window
  (e.g., 5 minutes) of your server's current time.
</Tip>

<Warning>
  Your client secret is provided when you register a webhook. Store it securely as an environment variable — never
  hard-code it in your application.
</Warning>

## Event Debouncing & Batching

Compliance status change webhooks use a **sliding-window debounce** to prevent webhook floods during bulk operations:

| Parameter      | Value          | Description                                     |
| -------------- | -------------- | ----------------------------------------------- |
| Sliding window | **1 minute**   | Timer resets with each new change event         |
| Maximum cap    | **30 minutes** | Forces delivery even during continuous activity |

**How it works:**

1. A compliance-relevant change is detected (e.g., license created, appointment updated)
2. Turris starts a 1-minute timer for the affected upstream entity
3. If another change occurs within that minute, the timer resets
4. When 1 minute passes with no new changes (or the 30-minute cap is reached), all accumulated changes are evaluated
5. A single webhook is sent containing **only the entity/product/state combinations where the compliance status actually changed**

<Note>
  **Compliance Data Synchronized** (`ENTITY_COMPLIANCE_DATA_SYNCHRONIZED`) and **Producer Agreement Executed**
  (`PRODUCER_AGREEMENT_EXECUTED`) webhooks are sent immediately — they are not debounced, since each is a discrete,
  one-time event.
</Note>

### Batched Payloads

Because changes are debounced, a single webhook delivery may contain multiple status changes in the `payload` array. Each element represents a distinct entity + product + state combination that changed. Design your handler to iterate over the full array.

## Reliable Delivery

### Automatic Retries

If your endpoint is unavailable or returns an error, we'll automatically retry delivery using exponential backoff:

| Attempt | Timing                        |
| ------- | ----------------------------- |
| 1       | Immediate                     |
| 2       | \~5 seconds                   |
| 3       | \~30 seconds                  |
| 4+      | Exponential backoff continues |

We'll attempt delivery up to 3 times over several hours.

### Manual Resend

You can manually resend any webhook event through the Turris Web App. This will cancel any pending automatic retries to prevent duplicates.

## Testing Your Integration

You can test your webhook integration in the following ways:

1. **Sandbox environment**: Configure webhooks with your development endpoint and trigger compliance changes in a sandbox environment to receive real webhook payloads
2. **Manual resend**: Use the Turris Web App to resend any previously delivered webhook event to your endpoint

This helps you verify your integration works before going live.

## Monitoring & Troubleshooting

### Delivery History

In our web app you can view the status of all webhook deliveries, including:

* Success/failure status
* Response times
* Number of retry attempts
* Error details

### Common Issues

| Issue                           | Solution                                                                        |
| ------------------------------- | ------------------------------------------------------------------------------- |
| Timeouts                        | Ensure your endpoint responds within 30 seconds                                 |
| SSL Errors                      | Use a valid SSL certificate for HTTPS endpoints                                 |
| Signature Verification Failures | Verify your client secret is correct and you're signing `{timestamp}.{rawBody}` |
| Duplicate Processing            | Use payload content (e.g., entity ID + state + product) for deduplication       |

## Best Practices

<CardGroup cols={2}>
  <Card title="Idempotency" icon="arrows-rotate">
    Use payload content (entity ID, state, product) to detect and handle duplicate deliveries gracefully.
  </Card>

  <Card title="Logging" icon="file-lines">
    Log all incoming webhook events for debugging and audit purposes.
  </Card>

  <Card title="Async Processing" icon="bolt">
    Respond quickly (2xx status) then process the event asynchronously to avoid timeouts.
  </Card>

  <Card title="Security" icon="shield">
    Store your client secret securely and consider IP whitelisting for additional security.
  </Card>
</CardGroup>
