---
title: "Signed Outbound Webhooks in HitKeep | HitKeep"
description: "Configure signed outbound webhooks in HitKeep, verify HMAC-SHA256 requests, choose site or instance events, inspect retries, and rotate secrets."
canonical: "https://hitkeep.com/guides/integrations/webhooks/"
---

# Signed Outbound Webhooks in HitKeep

External systems often need to react when a HitKeep site, goal, import, team, or user changes. Polling the REST API adds delay and repeated work. The HitKeep 2.11 webhook contract lets an owner or administrator subscribe an HTTPS endpoint to the operational events that matter.

Each delivery contains a versioned JSON body and four verification headers. HitKeep signs the exact request body with a one-time secret, retries unsuccessful deliveries, and keeps delivery outcomes for inspection. Delivery is at least once and ordering is not guaranteed, so receivers must verify, deduplicate, and process events idempotently.

## Verify A Delivery In Go

Read and verify the exact body bytes before decoding JSON. This example uses a five-minute timestamp tolerance as a receiver policy. Store processed event or delivery IDs in durable storage before returning a successful response.

```
package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "io"
  "log"
  "net/http"
  "strconv"
  "strings"
  "time"
)

const signingSecret = "whsec_replace_with_your_one_time_secret"

func main() {
  http.HandleFunc("POST /hitkeep/webhook", receiveHitKeepWebhook)
  log.Fatal(http.ListenAndServe(":8081", nil))
}

func receiveHitKeepWebhook(w http.ResponseWriter, r *http.Request) {
  body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
  if err != nil {
    http.Error(w, "invalid body", http.StatusBadRequest)
    return
  }

  timestamp := r.Header.Get("X-HitKeep-Timestamp")
  seconds, err := strconv.ParseInt(timestamp, 10, 64)
  if err != nil {
    http.Error(w, "invalid timestamp", http.StatusUnauthorized)
    return
  }
  delta := time.Since(time.Unix(seconds, 0))
  if delta < -5*time.Minute || delta > 5*time.Minute {
    http.Error(w, "stale timestamp", http.StatusUnauthorized)
    return
  }

  signature := r.Header.Get("X-HitKeep-Signature")
  if !strings.HasPrefix(signature, "v1=") {
    http.Error(w, "invalid signature", http.StatusUnauthorized)
    return
  }
  provided, err := hex.DecodeString(strings.TrimPrefix(signature, "v1="))
  if err != nil {
    http.Error(w, "invalid signature", http.StatusUnauthorized)
    return
  }

  mac := hmac.New(sha256.New, []byte(signingSecret))
  mac.Write([]byte(timestamp + "."))
  mac.Write(body)
  if !hmac.Equal(mac.Sum(nil), provided) {
    http.Error(w, "invalid signature", http.StatusUnauthorized)
    return
  }

  eventID := r.Header.Get("X-HitKeep-Event-ID")
  deliveryID := r.Header.Get("X-HitKeep-Delivery-ID")
  _ = eventID
  _ = deliveryID
  // Decode the JSON body, apply the event idempotently, and persist the ID.

  w.WriteHeader(http.StatusNoContent)
}
```

Do not parse and re-serialize the JSON before signature verification. Whitespace and object-key ordering are part of the signed bytes. This example listens on plain HTTP for local development; publish the production receiver through an HTTPS endpoint that meets HitKeep’s destination rules.

## Create A Webhook

Webhook administration is a human dashboard workflow. API clients cannot create or rotate webhooks.

1. Open **Integration -> Webhooks**.
2. Select **Site** for one site’s events or **Instance** for cross-site and administrative events.
3. Choose **Create webhook**, enter the HTTPS destination, and select at least one event.
4. Save the signing secret immediately. HitKeep shows it only after creation or rotation.
5. Use **Send test** and confirm that the receiver returns a `2xx` response.
6. Open **Deliveries** to inspect status, attempt count, response code, and retry outcomes.

![HitKeep Create webhook dialog with name, destination URL, description, enabled state, and site event checkboxes](https://hitkeep.com/_astro/integration-webhook-form.LhsW-ykM_eyRM9.webp)

Create and edit webhooks in the standard HitKeep dialog, with the valid event catalog loaded for the selected scope.

![HitKeep configured webhooks table showing a searchable endpoint, separated URL origin and path, enabled status, pagination, and a subscribed-events popover](https://hitkeep.com/_astro/integration-webhooks-events.mKIEElSI_2hzrG4.webp)

The webhook list follows the shared searchable and sortable table pattern. Open the event count to inspect the exact subscription without expanding the row.

Instance owners and instance administrators can manage instance webhooks. Site owners and site administrators can manage webhooks for their sites; instance owners and administrators can also manage site webhooks. Editors, viewers, instance users, and delegated API clients do not receive webhook-management permission. See [HitKeep roles and webhook permissions](https://hitkeep.com/guides/admin/permissions/) for the broader authorization model.

## Request Headers And Signature

| Header | Meaning |
| --- | --- |
| Content-Type | Always application/json |
| User-Agent | HitKeep-Webhook/<major.minor> for the running release line |
| X-HitKeep-Timestamp | Unix timestamp in seconds, included in the signed message |
| X-HitKeep-Signature | v1=<hex HMAC-SHA256> over timestamp + "." + exact_body |
| X-HitKeep-Event-ID | Stable event ID. Use this to deduplicate the same event across retries or endpoints when appropriate. |
| X-HitKeep-Delivery-ID | Stable delivery ID for one endpoint subscription. It is also present as delivery_id in the body. |

Use a constant-time comparison such as Go’s `hmac.Equal`. Reject missing or stale timestamps before doing application work. Keep the timestamp tolerance tight enough for your network path and clock synchronization.

## Payload Contract

Every delivery uses this envelope:

```
{
  "api_version": "2.11",
  "id": "5ec7ee31-7ddd-4bd9-a18d-0fb2d5730cc4",
  "delivery_id": "1d1b4f1d-693e-4534-8c2f-03d40ac11617",
  "type": "goal.converted",
  "created_at": "2026-07-10T14:30:00Z",
  "data": {
    "site_id": "cf4b9cab-6fa7-4288-b1ab-eac93bd46553",
    "goal_id": "6407f555-251a-44f9-a0aa-71c70b354859",
    "goal_name": "Trial signup",
    "goal_type": "event",
    "converted_at": "2026-07-10T14:30:00Z"
  }
}
```

`api_version` is the HitKeep major.minor release line that defines the webhook contract. `id` identifies the operational event. `delivery_id` identifies its delivery to one webhook. The fields inside `data` depend on the event type.

Use the [HitKeep REST API reference](https://hitkeep.com/api/) for the current `WebhookEventPayload`, webhook-management, and delivery-history schemas. A running instance exposes the same runtime contract at `/api/docs/v1/openapi.json`.

## Available Events

Site webhooks receive events for one site:

| Event | When it is emitted |
| --- | --- |
| site.updated | Site configuration changes |
| site.deleted | A site deletion completes |
| goal.created | A goal is created |
| goal.updated | A goal is updated |
| goal.deleted | A goal is deleted |
| goal.converted | A tracked pageview or custom event matches a goal |
| import.completed | An analytics import completes |
| import.failed | An analytics import fails |
| webhook.test | An administrator queues a test delivery |

Instance webhooks can subscribe to every site event above, plus these instance events:

| Event | When it is emitted |
| --- | --- |
| site.created | A site is created |
| system.user.updated | An instance user changes |
| system.user.deleted | An instance user is deleted |
| team.created | A team is created |
| team.updated | Team configuration changes |
| team.archived | A team is archived |
| team.member.added | A member joins a team |
| team.member.removed | A member leaves or is removed from a team |

The dashboard loads the event catalog for the selected scope, so it does not offer invalid combinations. The server also validates subscriptions on create and update.

## Delivery And Retry Behavior

| Behavior | Default |
| --- | --- |
| Success response | Any HTTP 2xx status |
| Request timeout | 10 seconds |
| Maximum attempts | 6 total attempts |
| Initial retry delay | 30 seconds |
| Maximum retry delay | 6 hours |
| Global delivery concurrency | 8 |
| Per-endpoint concurrency | 1 |
| Delivery-history retention | 30 days |
| Recovery sweep | Every 30 seconds |
| Redirects | Not followed |

Retries use exponential backoff capped by the configured maximum. Delivery is at least once, and different events may arrive out of order. A receiver should therefore:

1. Verify the timestamp and signature against the exact body.
2. Check whether the event or delivery ID was already processed.
3. Apply the change idempotently in a transaction or durable queue.
4. Persist the processed ID.
5. Return `2xx` only after durable acceptance.

HitKeep stores safe delivery outcomes and attempt metadata. The dashboard does not return saved request bodies or signing secrets in delivery history.

![HitKeep webhook delivery history dialog showing a successful webhook.test delivery, one attempt, HTTP 204 response, sortable columns, and pagination](https://hitkeep.com/_astro/integration-webhook-deliveries.OU4qcerD_Zw9P0R.webp)

Delivery history is paginated and sortable, with the event, terminal status, attempt count, response status, and creation time visible at a glance.

## Destination Safety

Production destinations must use HTTPS. HitKeep rejects URLs with credentials or fragments and blocks destinations that resolve to loopback, private, link-local, multicast, documentation, or other reserved addresses. It validates the destination when configured and again before delivery, uses direct DNS resolution, disables proxy inheritance for the webhook transport, and does not follow redirects.

For an explicit local development test, `HITKEEP_WEBHOOK_ALLOW_DEVELOPMENT_TARGETS=true` permits HTTP and private destinations. Do not enable it on a production instance. It removes the outbound destination protections that prevent server-side request forgery.

See the [HitKeep security model](https://hitkeep.com/reference/security/) for the complete list of optional outbound connections and restricted-network considerations.

## Rotate Or Revoke A Secret

Choose **Rotate secret** when a receiver secret may be exposed or as part of a regular credential policy. HitKeep replaces the configured secret, re-signs pending, retrying, and processing delivery records with the new value, and shows the replacement once. A request already in flight may still carry the previous signature, so coordinate a short overlap window at the receiver when required.

Deleting a webhook stops new subscriptions. Existing delivery outcome records remain until retention cleanup. Because HitKeep must sign deliveries, configured and snapshotted delivery secrets are stored in the HitKeep database. Protect the full data directory and every backup as credential-bearing data.

## Outbound Webhook Configuration

Most installations can keep the defaults. Operators with slow receivers or strict concurrency limits can tune the outbound worker through the [configuration reference for webhook delivery](https://hitkeep.com/reference/configuration/#outbound-webhook-delivery).

| Environment variable | Default | Purpose |
| --- | --- | --- |
| HITKEEP_WEBHOOK_ALLOW_DEVELOPMENT_TARGETS | false | Allow HTTP and private destinations for explicit development testing |
| HITKEEP_WEBHOOK_DELIVERY_TIMEOUT | 10 | Request timeout in seconds |
| HITKEEP_WEBHOOK_DELIVERY_CONCURRENCY | 8 | Maximum concurrent outbound deliveries |
| HITKEEP_WEBHOOK_PER_ENDPOINT_CONCURRENCY | 1 | Maximum concurrent deliveries for one webhook |
| HITKEEP_WEBHOOK_MAX_ATTEMPTS | 6 | Total delivery attempts before failure |
| HITKEEP_WEBHOOK_RETRY_BASE_SECONDS | 30 | Initial retry delay |
| HITKEEP_WEBHOOK_RETRY_MAX_SECONDS | 21600 | Maximum retry delay |
| HITKEEP_WEBHOOK_RETENTION_DAYS | 30 | Delivery-history retention |
| HITKEEP_WEBHOOK_SWEEP_SECONDS | 30 | Pending-delivery recovery interval |

## Related Documentation

- [HitKeep REST API reference](https://hitkeep.com/api/) for the generated webhook schemas and dashboard-session endpoints
- [Runtime facts and product limits](https://hitkeep.com/reference/facts-and-limits/) for the canonical webhook security and delivery boundary
- [HitKeep roles and permissions](https://hitkeep.com/guides/admin/permissions/) for site and instance administration
- [Configuration reference](https://hitkeep.com/reference/configuration/) for every flag and environment variable
- [Server-side event tracking](https://hitkeep.com/guides/tracking/server-side-tracking/) when another system needs to send events into HitKeep instead

Self-hosted HitKeep includes the webhook worker in the same binary as the dashboard, queue, and database. If your team wants signed event delivery without owning upgrades, backups, and worker operations, [start a managed HitKeep Cloud deployment](https://cloud.hitkeep.eu/signup?plan=free&billing=monthly&utm_source=hitkeep_docs&utm_medium=referral&utm_campaign=cloud_signup&utm_content=docs_inline).

[Previous WordPress integration](https://hitkeep.com/guides/integrations/wordpress/)[Next MCP server](https://hitkeep.com/guides/integrations/mcp/)
