Skip to content

Raise bugs from any tool with a webhook

Most error trackers, monitors and form tools can send a webhook but cannot send an API key. Give the app a webhook URL, point the tool at it, and every POST becomes a bug — no code, and no integration of its own.

The shape of it

  1. Each live app can have a private URL, like https://qarunbook.com/api/integrations/webhook/k3x9….
  2. Paste it into the tool as a webhook. Each POST it sends becomes an issue on the app, and the answer carries its code.
  3. The body can be in our own shape, or in the tool's: pick a preset for Bugsnag, Rollbar or a Datadog monitor, or type the paths to the fields you want.
  4. The same error sent again while its issue is open is not raised twice.

It is the same issue as one raised in the app, so everything else follows: it is filed in Linear, Jira, GitHub or GitLab if the app is connected, Slack hears of it, and it counts towards Free's monthly allowance of issues. The webhook is on every plan.

If your code can set an Authorization header and you want checks, results and lists too, use the REST API instead. The webhook is for tools that can only POST to a URL.

Turn it on

  1. Open the app, choose Configure, then the Integrations tab, and find Incoming webhook. You need to be an admin of the app.
  2. Pick the payload the tool sends, and choose Turn on. The URL appears; Copy it into the tool.

The webhook is for apps in live mode, where a bug does not need a check — a monitoring tool cannot say which check failed. Switch the app to live under Settings first. If it goes back to testing, deliveries are refused with 409 not_live.

Our own shape

Only title is needed. Everything else is optional:

  • title — the first line of the issue. Without one, the first line of description is used.
  • description — the rest of it.
  • platforms — the platforms it affects: a list or a comma-separated string of the app's codes or names. ios or android work when the app has one platform of that kind. Words that match nothing, like production, are ignored, and the card's default applies.
  • priority — urgent, high, medium or low. Severities are understood too: critical and fatal are urgent, error is high, warning medium, info low, and Datadog's P1–P5 and Rollbar's numeric levels map the same way. Anything else gets the card's default.
  • url — the page it happened on, added to the issue's text.
  • reporter — { name, email? }, someone outside the team. The issue reads Ada Lovelace via webhook and the REST API returns her as reporter.
  • external_id — the tool's own id for the problem, for dedupe.
  • link — a link back to it in the tool, shown on the issue. http and https links only.
  • source — a short name for the tool, kept with the issue. Presets fill it in themselves.
Raise a bug
curl -X POST "https://qarunbook.com/api/integrations/webhook/<token>" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Checkout button does nothing",
    "description": "Tapping Pay shows a spinner forever.",
    "platforms": ["IOS"],
    "priority": "high",
    "url": "https://shop.example.com/checkout",
    "reporter": { "name": "Ada Lovelace", "email": "ada@example.com" },
    "external_id": "ticket-4812",
    "link": "https://support.example.com/tickets/4812"
  }'
Raised
HTTP/1.1 201 Created

{ "issue": { "id": "m1x…", "code": "QA-41", "url": "https://qarunbook.com/?app=…&bug=…" } }

JSON is read whatever the Content-Type says, since some tools send text/plain. A form post works too, as its fields, or with the JSON in a payload field.

Presets

A preset knows where a tool keeps the title, the id, the link and the severity in its own webhook. Anything it does not find falls back to our shape, so the fields above still work alongside it.

Bugsnag

In Bugsnag, open the project's Integrations and email settings, add a Webhook, paste the URL, and choose which events notify it. The title is the error class and message; the error's id dedupes, its page in Bugsnag is the link, and its severity is the priority. See Bugsnag's webhook docs.

Rollbar

In Rollbar, open the project's Notifications, add the Webhook channel with the URL, and pick the rules — New item and Reactivated item are the useful ones. The item's title, id, link and level are read; every occurrence of one item dedupes onto one issue. See Rollbar's webhook docs.

Datadog monitors

In Datadog, add a webhook in the Webhooks integration with the URL, and use this payload — the default one plus three fields. Then mention @webhook-qarunbook (or whatever you named it) in a monitor's message. The aggregation key is the same for an alert and its recovery, so they land on one issue. See Datadog's webhook docs.

Datadog webhook payload
{
  "title": "$EVENT_TITLE",
  "body": "$EVENT_MSG",
  "id": "$ID",
  "aggreg_key": "$AGGREG_KEY",
  "link": "$LINK",
  "priority": "$ALERT_PRIORITY"
}

Any other payload

Choose Custom mapping and type where each field is, as a dot path into the JSON. Lists take an index: items[0].title or items.0.title. Two more forms help with real payloads:

  • error.errorId|error.id — the first of these that has a value.
  • {error.class}: {error.message} — a template. Placeholders are filled in, a line whose placeholders are all empty is left out, and stray separators at the ends are trimmed.
A custom mapping
// A payload from some tool…
{
  "event": "error.created",
  "data": {
    "error": { "id": "e_93", "message": "Payment failed", "level": "critical", "os": "Android" },
    "permalink": "https://monitor.example.com/e_93"
  }
}

// …and the paths that read it, typed into the card:
Title        data.error.message
Description  Level {data.error.level} on {data.error.os}
Link back    data.permalink
External id  data.error.id
Priority     data.error.level
Platform     data.error.os

Dedupe

Error trackers send the same error more than once — a spike, a retry, a reopened item. When a delivery carries an external id and an issue from the same source with that id is still open, nothing new is raised: the answer is 200 with "duplicate": true and the existing issue. Once that issue is marked fixed, the next delivery raises a new one, because the error is back.

Already open
HTTP/1.1 200 OK

{ "duplicate": true, "issue": { "id": "m1x…", "code": "QA-41", "url": "https://qarunbook.com/?app=…&bug=…" } }

Without an external id, every delivery is its own issue.

Signing

Anyone who has the URL can raise a bug — that is what lets a tool with no settings for keys use it. If the tool can sign what it sends, add a signing secret on the card: Make a secret, or paste the one your tool gave you. From then on a delivery must carry

Header
X-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the secret>

and anything without it, or with the wrong one, is refused with 401 invalid_signature. Sign the exact bytes you send — re-encoding the JSON after signing changes them.

curl, signed
BODY='{"title":"Checkout button does nothing"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$QARUNBOOK_WEBHOOK_SECRET" | sed 's/^.* //')

curl -X POST "https://qarunbook.com/api/integrations/webhook/<token>" \
  -H "Content-Type: application/json" \
  -H "X-Signature: sha256=$SIG" \
  -d "$BODY"
Node, signed
import { createHmac } from "node:crypto";

const body = JSON.stringify({ title: "Checkout button does nothing" });
const signature = "sha256=" + createHmac("sha256", process.env.QARUNBOOK_WEBHOOK_SECRET).update(body).digest("hex");

await fetch(process.env.QARUNBOOK_WEBHOOK_URL, {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-Signature": signature },
  body, // exactly the string that was signed
});

Regenerate URL makes a new one and stops the old one at once, for when it has ended up somewhere it should not be. Turn off refuses everything until you turn it back on; the URL stays the same.

Hearing back when it is fixed

Give the card a Notify URL, and when a bug that came in through the webhook is marked fixed — from any door: the app, MCP, the API, a merged pull request, Linear or Jira — or its fix is verified, we POST it there, signed with the same secret in the same header. Use it to resolve the error in the tool it came from. It is sent once, with a five-second timeout, and redirects are not followed. The notify URL must be public HTTPS, and needs a signing secret.

What the notify URL receives
POST https://hooks.example.com/qarunbook
Content-Type: application/json
X-Qarunbook-Event: issue.fixed
X-Signature: sha256=…

{
  "event": "issue.fixed",
  "at": "2026-10-05T09:12:00.000Z",
  "app": { "id": "…", "name": "Checkout" },
  "issue": {
    "id": "m1x…", "code": "QA-41", "url": "https://qarunbook.com/?app=…&bug=…",
    "external_id": "5f3e9a", "source": "bugsnag", "link": "https://app.bugsnag.com/…",
    "status": "fixed", "fixed_by": "Sam", "fixed_at": "…", "verified_by": null, "verified_at": null
  }
}

Answers

  • 201 — raised. { issue: { id, code, url } }.
  • 200 — already open, see dedupe.
  • 400 invalid_json — the body is not a JSON object. 400 missing_title — nothing to call the issue.
  • 401 invalid_signature — the app has a signing secret and the signature is missing or wrong.
  • 402 limit_reached — the workspace has used this month's issues on Free.
  • 404 not_found — no webhook at that URL: it is off, was replaced, or never existed.
  • 409 not_live — the app is not in live mode. 409 in_progress — the same external id is being raised by another delivery at this moment; retry.
  • 413 too_large — the body is over 1 MB.
  • 429 rate_limited — over 120 deliveries this hour to one app. Wait for the hour to turn.

Errors are { error: { code, message } }, the same as the REST API's.

Something unclear or missing? Write to hello@qarunbook.com.