Hydra logoHydra · Hydra Support

Configuration

Webhooks

Subscribe an HTTPS endpoint to Hydra events for real-time syncing with Zapier, n8n, or your own automation.

Webhooks

Webhooks let an external system listen for Hydra events in real time. When a customer is created, a ticket gets resolved, or an account moves to "at risk," Hydra POSTs a signed JSON payload to the URL you've registered. This is the fastest way to wire Hydra into Zapier, n8n, or a custom integration without polling the REST API.

Where to manage them

Settings → Webhooks. From there you can create, pause, edit, send a test event, view recent failures, rotate the signing secret, and delete a subscription.

What gets sent

Every event POST has the same envelope:

{
  "event": "ticket.created",
  "tenant_id": "...",
  "data": {
    "entity_type": "ticket",
    "entity_id": "...",
    "entity": { /* the full entity row, same shape as the public REST API */ }
  },
  "timestamp": "2026-04-29T18:33:00Z"
}

The HTTP request also carries:

  • Content-Type: application/json
  • X-Hydra-Event: <event> — for quick branching without parsing the body
  • X-Hydra-Signature: t=<unix_ts>,v1=<hex> — see "Verifying signatures" below
  • User-Agent: Hydra-Webhooks/1.0

The request times out after 8 seconds. Anything in the 200–299 range counts as success; everything else is a failure and triggers a retry.

Available event types

The event picker in Settings → Webhooks is the source of truth, but the current set is:

EventWhen it fires
customer.createdA new lead or contact is created (any source — widget, CSV import, MCP, public API).
account.createdA new account is created.
account.stage_changedAn account's lifecycle_stage changes (prospect → active, etc.).
account.went_at_riskAn account specifically transitions into at_risk — useful for ops alerts.
conversation.createdA new conversation is opened (widget, email, etc.).
conversation.resolvedA conversation is marked resolved.
conversation.assignedA conversation is assigned to an agent.
ticket.createdA new ticket is opened.
ticket.assignedA ticket is assigned to an agent.
ticket.resolvedA ticket transitions to resolved.
ticket.status_changedA ticket transitions to any non-resolved status.

A subscription that subscribes to multiple events receives one POST per event — no batching.

Verifying signatures

Every request carries an X-Hydra-Signature header in the form t=<unix_ts>,v1=<hex>. To verify:

  1. Compute HMAC_SHA256(signing_secret, "${t}.${rawBody}"). Use the EXACT raw bytes of the request body — don't re-serialize from a parsed JSON, that introduces whitespace differences.
  2. Compare in constant time against the v1 value.
  3. Reject if |now - t| > 300 seconds to defeat replay attacks.

Node example:

const crypto = require('crypto')

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest()
  const provided = Buffer.from(parts.v1, 'hex')
  if (provided.length !== expected.length) return false
  if (!crypto.timingSafeEqual(provided, expected)) return false
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
  return true
}

The signature shape mirrors Stripe's Stripe-Signature so the same library patterns apply.

Signing secrets

When you create a webhook, Hydra generates a 32-byte random secret prefixed whsec_…. It's shown once at creation time and never again. If you lose it, click "Rotate secret" to mint a new one — your endpoint must be updated immediately, or signature verification will fail on the next event.

In the subscription list view, only the prefix of the secret is shown so you can confirm which one you have.

Retry behavior

When a delivery fails (network error, timeout, or any non-2xx response), Hydra schedules a retry. The default schedule is exponential, capped at 1 hour:

AttemptDelay since previous
1(immediate)
2~30 seconds
3~2 minutes
4~10 minutes
5~30 minutes
6~1 hour

After six total attempts, the delivery is marked failed and removed from the retry queue. The full failure history (status code, error message, response time per attempt) is visible in Settings → Webhooks → History.

Successful inline deliveries don't write history rows — only failures and their retries do — so the history view is intentionally focused on what needs your attention.

Testing

The "Send test" button on each subscription posts a synthetic webhook.test event to your endpoint, signed with your real secret. The response status code and body are surfaced inline so you can validate the wiring without waiting for a real event to fire.

Disabling temporarily

The "Pause" button flips active=false on the subscription. Pending retries stop firing, no new events get queued, and your endpoint goes silent. Resume to re-enable; queued events that fired during the pause are not replayed.

Permissions

Creating, editing, and deleting subscriptions is admin-only (Owner or Admin role). Members can see them in the Settings list but cannot modify them.

Troubleshooting

  • All my deliveries are failing with 401/403 — your endpoint is rejecting the request. Check that you're verifying signatures correctly; many libraries default to URL-encoded form parsing, which strips the raw bytes the signature needs.
  • Signature validation fails on every request — make sure you're hashing the RAW body, not a re-serialized version. Express's json() middleware re-emits without preserved whitespace; you need express.raw() or equivalent.
  • Some events don't fire — confirm the event type is in your subscription's event_types list. The Settings UI shows the full set of available events; if you don't see one you expect, it may not have an emitter wired yet.
  • Pending retries piling up — check the History panel; the most recent error message (from the latest attempt) tells you why your endpoint is rejecting. The retry queue auto-drains as the endpoint recovers.