Metrickle

Outgoing webhooks

Last updated

A webhook sends a signed JSON request to your own endpoint when something happens in an app: a problem report, a broken protected flow, a case opened or closed, a study booked. Use it to start your own workflows, or to feed a tool Metrickle doesn't post to directly. Slack and Microsoft Teams are set up the same way and get a formatted message instead. See integrations for those.

Add a webhook

  1. Open the app's Integrations page and choose Add Slack, Teams or webhook under Notify. You need to be an owner or admin, or have the integrations.manage permission.
  2. Choose Webhook, give it a name, paste your endpoint's URL and pick the events to send.
  3. Copy the signing secret (whsec_…). It's shown once.
  4. Send a test delivery to check your endpoint answers.

The URL must start with https:// and use a public host name, not localhost or an IP address. An app can have up to 20 destinations.

The request

Each delivery is a POST with a JSON body:

POST /your/endpoint
content-type: application/json
user-agent: Metrickle-Webhooks/1
metrickle-event: feedback.created
metrickle-delivery: evt_…
metrickle-signature: t=1790841234000,v1=5f2c…

{
  "id": "evt_…",
  "type": "feedback.created",
  "createdAt": 1790841234000,
  "app": { "id": "app_…", "name": "Orbit Web" },
  "url": "https://app.metrickle.com/apps/app_…/feedback",
  "data": {
    "feedbackId": "…",
    "category": "accessibility",
    "accessibilityBarrier": true,
    "message": "The pay button can't be reached with Tab",
    "path": "/checkout",
    "a11y": ["keyboard"]
  }
}
FieldMeaning
idA unique id for this delivery, the same as the metrickle-delivery header
typeThe event type, the same as the metrickle-event header
createdAtWhen it was sent, in Unix milliseconds
appThe app it's about
urlWhere to look in the dashboard
dataThe event's details (below)

Check the signature

metrickle-signature is t=<unix ms>,v1=<hex>. The hex is an HMAC-SHA256, keyed with your signing secret, of the timestamp, a full stop and the raw request body. Check it on every request, and reject requests more than five minutes old:

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

export function verify(rawBody: string, header: string, secret: string): boolean {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() - Number(t)) > 5 * 60_000) return false; // reject replays
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return typeof v1 === "string" && v1.length === expected.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Use the body exactly as it arrived. Parsing and re-encoding the JSON changes it and the check fails.

Responses and retries

Any 2xx response within 5 seconds counts as delivered. A network error, a timeout, a 408, a 429 or a 5xx is tried twice more, after 1 second and then 4 seconds. Other responses aren't retried. Each destination's delivery log, on the Integrations page, shows the last 50 deliveries and what your endpoint answered.

Event types

TypeSent whendata fields
feedback.createdSomeone sends a problem report or other feedback, with accessibility barriers called outfeedbackId, category, accessibilityBarrier, message, rating, path, url, platform, appVersion, device, a11y, hasScreenshot, source
alert.task_success_dropA task's success rate over the last 24 hours fell well below its 7-day baselinetaskId, taskName, appId, current, baseline, worstSegment, abandonedAt
alert.new_hotspotAn element or form started causing friction in the last 24 hourssignal, path, target, label, sessions, events
alert.contract_brokenA protected flow broke: people using an accessibility setting fall clearly behind people using none since the latest releasetaskId, taskName, appId, verdict, segment, baseline, comparedWith, release, window
case.openedA case opened on a broken task: the step, element and group of people that lose most, and what it costscaseId, taskId, taskName, status, finding, baseline, recordings, openedBy
case.closedA case closed: its fix verified for the people it broke for, or dismissedThe case.opened fields, plus resolution (verified or dismissed), after, closedAt, closeNote
issue.measured7 days after a linked issue is closed: the measure before and after the fixissueKey, issueUrl, title, improved, before, after
study.bookedSomeone booked, moved or cancelled a moderated research sessionchange (booked, moved or canceled), studyId, participantId, startsAt, endsAt, hasAccessNeeds
study.completedA participant finished an unmoderated test, or a moderator wrapped up a sessionstudyId, participantId, kind, tasksDone

Protected flows are called contracts in the API, which is why the type is alert.contract_broken.

alert.task_success_drop and alert.new_hotspot also carry latestRelease (version, at, url) when there was a release in the 2 days before, since it's often the cause.

For feedback.created, source is { "label", "url" } when the report came from a connected support inbox, such as Zendesk or Intercom, and null for reports sent in the app.

A test delivery has the type test and data of { "message": "This is a test delivery." }.

How often alerts are sent

Alerts are checked hourly. Each task alert is sent at most once a day, and each friction hotspot at most once a week.