Webhooks

Register signed, real-time event endpoints and verify their signatures.

POST/api/v1/webhookssecret key

Webhooks

A webhook turns the flow around: instead of your code repeatedly asking “anything new?”, Sentriment calls you the moment something happens. You give us a URL; we POST a signed JSON event to it. What teams build with the events:

  • Alert the room — on cluster.regressed, post to Slack or page on-call: an issue you marked fixed is being reported again. This is the event most customers wire first.
  • File the work — on feedback.processed where type is bug, open a Linear/Jira ticket with the redacted quote and sentiment already attached; route feature_request items to your roadmap tool instead.
  • Close the loop with users — strongly negative items (check sentimentScore) can trigger a personal follow-up from support before the user churns; praise can trigger a review-request email while goodwill is high.
  • Watch a decision — on question.verdict_changed, get told the day “what do users think of our pricing?” flips from mixed to negative after a pricing change — instead of discovering it in a quarterly review.
  • Keep systems in sync — stream every processed item into your data warehouse or CRM so feedback sits next to revenue and usage data.

Mechanics: register up to 10 HTTPS endpoints per project. Every delivery is HMAC-signed (verify it — see below), retried 5× with increasing delays if your server is down, and an endpoint that keeps failing for 15 minutes straight is switched off automatically so a dead URL can't pile up retries — a burst of failures alone won't do it, so an endpoint that is rate-limiting us rather than broken stays enabled. GET /webhooks lists your endpoints with delivery health, including failingSince; PATCH /webhooks/{id} pauses or resumes one; and DELETE /webhooks/{id} removes it for good.

ParameterInDescription
feedback.processedeventAn item finished AI analysis — the payload carries the full analysis.
cluster.spikingeventA theme crossed its spike threshold: 4+ reports in 24h at 3× the prior week's daily rate.
cluster.regressedeventA theme you marked resolved received new feedback. Your 'it's back' alarm.
cluster.resolvedeventYou marked a theme resolved — payload includes the requester user ids, so you can tell them it shipped.
user.health_changedeventAn identified user just entered the at-risk band (health score below 40). Fires on the way in, not on recovery.
question.verdict_changedeventA tracked Ask question's verdict flipped (e.g. mixed → negative).
Create an endpoint
curl -X POST https://app.sentriment.com/api/v1/webhooks \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://api.example.com/sentriment", "events": ["cluster.regressed"] }'

# → { "id": "01KX…", "secret": "whsec_…", … }   ← store the secret; shown once
Pause one, then bring it back
curl -X PATCH https://app.sentriment.com/api/v1/webhooks/01KX… \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "active": false }'

# Resuming also clears the consecutive-failure count, which is how you
# revive an endpoint that was auto-disabled after an outage.
curl -X PATCH https://app.sentriment.com/api/v1/webhooks/01KX… \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "active": true }'
Delivery payload
{
  "id": "evt_…",
  "event": "cluster.regressed",
  "occurredAt": "2026-07-12T14:07:44.000Z",
  "data": { "clusterId": "01KX…", "label": "Search · Slow query response", "triggeredByFeedbackId": "01KX…" }
}

Verify signatures

Anyone who discovers your webhook URL could send it fake events — the signature is how you know a delivery genuinely came from Sentriment and wasn't tampered with in transit. Verification is ~10 lines and should reject anything that fails. Every delivery includes X-Sentriment-Signature: t=<unix>,v1=<hex> where v1 = HMAC-SHA256(secret, `${t}.${rawBody}`). Verify against the raw request body and reject timestamps older than 5 minutes:

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, signatureHeader, secret) {
  const { t, v1 } = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=", 2)),
  );
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // stale
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Was this page helpful?