Skip to main content
HoundDog API Documentation

Webhooks

A webhook subscription asks HoundDog to POST to your endpoint whenever an event of a given type happens on your account. Subscriptions are managed on the Webhooks page of the dashboard or through the API (/api/v2/webhook_subscriptions). Webhook subscriptions created before v2 keep their current payload unchanged; they also get the address check before every delivery and the Webhook Delivery Failing event described below.

Requirements

  • The endpoint must be https://.
  • The endpoint may not resolve to a private, loopback, link-local or cloud-metadata address. We check at creation and again before every delivery, and each delivery connects only to the addresses that check approved.
  • Event types come from the catalog (GET /api/v2/webhook_event_types): every customer-visible event type the app records. Internal, staff-only events are never delivered.

Payload

{
  "event_id": 123456,
  "event": "Vendor Became Compliant",
  "occurred_at": "2026-10-07T14:03:11Z",
  "account_id": 4127,
  "party_id": 191537,
  "certificate_id": null,
  "project_id": null,
  "user_id": null,
  "api_key": null,
  "w9_id": null,
  "signing_request_id": null,
  "signing_template_id": null,
  "parent_event_id": null,
  "message": "Acme Electric became compliant",
  "data": { },
  "links": { "party": "/api/v2/parties/191537" }
}
Member Meaning
event_id, event, occurred_at, account_id, message, data, links The event (same id as in Events), its type, and a readable sentence
party_id, certificate_id, project_id, user_id, api_key What the event is about and who acted, when they apply
parent_event_id The event this one belongs to; set on the email-health child events (Email Skipped (Suppressed), Email Recipients Skipped), which carry no party or request id of their own
w9_id The W-9 a W-9 event is about, when the event carries one; null otherwise
signing_request_id, signing_template_id The signing request and template a signing event is about, null for other events

data is the event's structured details when they parse as JSON, else {"text": "<details>"}. A Party Edited delivery carries {"changes": [...]} there: every field the edit changed with its old and new value, in the shape described under Edit details. A version 1 subscription receives the event too, with the message naming the changed fields and no details. The custom field definition events (Custom Field Definition Created, Updated, Archived, Restored and Deleted, in the catalog's "Custom fields" group) carry the field there, in the shape described under Custom field events; they are account-level, so party_id is null. Email deliverability events (bounce, complaint, block) include the address and reason there. A W-9 never appears in a payload with its tax identification number.

Fields of features your account does not use are present with an empty value. links.party is omitted for an event about one of your individuals. Such an event is still delivered, but its ids (the event, the individual, a certificate) answer 404 on the v2 reads, which never serve individuals.

Signature

Every delivery from a v2 subscription carries:

Header Value
X-HoundDog-Signature t=<unix seconds>,v1=<hex HMAC-SHA256>
X-HoundDog-Event the event type
X-HoundDog-Delivery-Id stable for one event and subscription across retries
X-HoundDog-Attempt 1 on the first attempt, incrementing on retries
X-HoundDog-Payload-Version 2

The signing secret is shown once when the subscription is created and can be rotated (POST /api/v2/webhook_subscriptions/:id/rotate_secret, or the Rotate button on the Webhooks page). To verify a delivery:

  1. Read t and v1 from the signature header.
  2. Compute HMAC-SHA256(secret, "<t>.<raw request body>") and compare it to v1 with a constant-time comparison.
  3. Reject deliveries whose t is more than 5 minutes from your clock, to defeat replay.
t, v1 = header.split(",").map { |kv| kv.split("=", 2).last }
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{t}.#{raw_body}")
valid = ActiveSupport::SecurityUtils.secure_compare(expected, v1) && (Time.now.to_i - t.to_i).abs <= 300

Answer 2xx quickly and do your work afterwards; we wait 30 seconds for a response.

Retries and failure

  • A connection error, a timeout, or a failed DNS lookup of your host is retried with exponential backoff for about three weeks.
  • A 5xx response to a version 2 subscription is retried on the same schedule, and so is a version 2 delivery that could not be signed (signing failed or signing unavailable in the delivery log; signing unavailable is a temporary problem on our side and never counts toward disabling the subscription).
  • Any other non-2xx response to a version 2 subscription, and every non-2xx response to a version 1 subscription, is recorded as a failed attempt and not retried.
  • The first failed attempt in a run of failures writes a Webhook Delivery Failing event to your account's event log (visible in the dashboard and itself subscribable). It names the subscription by id, event type and host only, never the endpoint's path or query, which often carry a hook's token. A failed delivery of a Webhook Delivery Failing or Webhook Disabled event itself still counts toward the failures but writes no further failing event.
  • A version 2 subscription whose every attempted delivery for 7 days failed is disabled with a Webhook Disabled event (version 1 subscriptions are never disabled); re-enable it from the Webhooks page once your endpoint is healthy. Only real deliveries count toward this and toward a run of failures: a test delivery never starts a run or ends one, so a successful test does not reset a failing subscription.
  • Recent deliveries, with status codes and durations, are listed at GET /api/v2/webhook_subscriptions/:id/deliveries; POST /api/v2/webhook_subscriptions/:id/test sends a test delivery (the HTTP request is limited to 10 seconds in total; a host whose DNS does not answer can add a few seconds before it).