Skip to main content

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

1

Set up your endpoint

Create an endpoint to receive POST requests
2

Register your webhook URL

Register your webhook URL using our API
3

Verify signatures

Verify webhook signatures using your client secret
4

Handle events

Process events based on the payload data
5

Test integration

Test your integration using our test webhook feature

Available Webhook Events

Agent Compliance Status Change

Triggered when an agent’s compliance status changes due to license, appointment, or requirement updates.

Agency Compliance Status Change

Triggered when a downstream entity’s (agency) compliance status changes due to license, appointment, or requirement updates.

Compliance Data Synchronized

Triggered when NIPR data synchronization completes for an agent or downstream entity.

Producer Agreement Executed

Triggered when a downstream entity and your upstream entity have fully executed a producer agreement.

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:
{
  "webhookType": "WEBHOOK_TYPE",
  "upstreamEntityId": "507f1f77bcf86cd799439010",
  "payload": { ... }
}
FieldDescription
webhookTypeThe event type (e.g., AGENT_COMPLIANCE_STATUS_CHANGE)
upstreamEntityIdYour upstream entity ID
payloadEvent-specific data — see each event’s documentation for details

Setting Up Your Webhook Endpoint

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

Example Endpoint Response

{
  "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:
HeaderDescription
Content-Typeapplication/json
X-Turris-SignatureHMAC-SHA256 hex signature of the payload
X-Turris-TimestampUnix timestamp (seconds) when the request was signed
The request body is sent as plaintext JSON — no decryption is required.
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.

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)

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

// 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' });
  },
);
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.
Your client secret is provided when you register a webhook. Store it securely as an environment variable — never hard-code it in your application.

Event Debouncing & Batching

Compliance status change webhooks use a sliding-window debounce to prevent webhook floods during bulk operations:
ParameterValueDescription
Sliding window1 minuteTimer resets with each new change event
Maximum cap30 minutesForces 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
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.

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:
AttemptTiming
1Immediate
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

IssueSolution
TimeoutsEnsure your endpoint responds within 30 seconds
SSL ErrorsUse a valid SSL certificate for HTTPS endpoints
Signature Verification FailuresVerify your client secret is correct and you’re signing {timestamp}.{rawBody}
Duplicate ProcessingUse payload content (e.g., entity ID + state + product) for deduplication

Best Practices

Idempotency

Use payload content (entity ID, state, product) to detect and handle duplicate deliveries gracefully.

Logging

Log all incoming webhook events for debugging and audit purposes.

Async Processing

Respond quickly (2xx status) then process the event asynchronously to avoid timeouts.

Security

Store your client secret securely and consider IP whitelisting for additional security.