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.

Before you start

  • A tool that can send an HTTP POST to a URL — Bugsnag, Rollbar, a Datadog monitor, a form tool, your own script — and permission to add a webhook in it.
  • An app in live mode: in qarunbook, App configuration → Settings → What this app is for → Tracking bugs in a live app. A monitoring tool cannot say which check failed.
  • To be an admin of the app in qarunbook. The webhook is on every plan.

Connect an app, step by step

  1. In qarunbook

    Open the app's webhook settings

    Open the app, choose App configuration, then the Integrations tab, and choose the Incoming webhook tile under Where errors are caught.

  2. In qarunbook

    Pick the payload, and turn it on

    Under Payload, pick what the tool sends: qarunbook (native), Bugsnag, Rollbar, Datadog monitor or Custom mapping (see Presets).

    Optionally pick the platforms under When the payload names no platform, mark it as affecting, and a priority under When the payload names no priority. Choose Turn on.

  3. In qarunbook

    Copy the URL

    The card now shows This app's webhook URL. Choose Copy.

  4. In qarunbook

    Optionally, sign deliveries

    If the tool can sign what it sends, under Signing choose Make a secret and copy it — it is not shown again — or paste the tool's own secret and choose Use it. From then on, unsigned deliveries are refused (see Signing).

    With a secret set, you can also give a Notify URL (optional) to hear back when a bug is fixed (see Hearing back), and choose Save.

  5. In Your tool

    Paste the URL into your tool

    Add a webhook in the tool and paste the URL. Where that is, for the presets:

    • Bugsnag: Project settings → Integrations and email → Data forwarding → Webhook.
    • Rollbar: Settings → Notifications → Webhook.
    • Datadog: Integrations → Webhooks → New, with the payload below; then mention @webhook-name in a monitor's message.

    Any other tool: wherever it keeps outgoing webhooks, with the method POST.

Check it works

A test bug in our own shape, with curl. Only title is needed; swap in your URL and one of the app's platform codes.

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": "[email protected]" },
    "external_id": "ticket-4812",
    "link": "https://support.example.com/tickets/4812"
  }'
  1. In Your terminal

    Send the test bug

    Run the command above with the URL you copied.

  2. In Your terminal

    Read the answer

    A 201 with the new issue's code and link: {"issue":{"id":"…","code":"QA-41","url":"…"}}.

  3. In qarunbook

    See the bug

    The app has a new bug, Checkout button does nothing, reading Ada Lovelace via webhook, with the page and a link back to the ticket.

  4. In Your terminal

    Send it again

    While the issue is open, the same external_id is not raised twice: the answer is 200 with "duplicate": true and the existing issue. A body with no title is answered 400 missing_title, and a wrong URL 404.

  5. In Your tool

    Try it from your tool

    Use the tool's own test button where it has one, and look for the bug on the app.

How it works

  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 and Microsoft Teams hear of it, and it counts towards Free's monthly allowance of issues.

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.

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.

The test above sends every one of them, and is answered:

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. The card lists the paths each preset reads, with a link to the tool's docs.

Bugsnag

In Bugsnag, open the project's Project settings → Integrations and email → Data forwarding, 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 Settings → 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, go to Integrations → Webhooks, add a new webhook 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 — Title, Description, Link back, External id, Priority and Platform — 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

Change the preset or the paths on a webhook that is already on, then choose Save.

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 and choose Use it. 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
});

Make a new secret replaces it; Remove secret takes unsigned deliveries again and clears the notify URL.

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.

Good to know

  • The webhook only brings bugs in, and with a notify URL sends word back when they are fixed. It does not create checks or sections.
  • The URL is the permission. Regenerate URL, then Make a new 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 choose Turn on again; the URL stays the same.
  • If the app goes back to testing, deliveries are refused with 409 not_live until it is live again.
  • A bug that came in through the webhook shows a link back to it in the tool — Bugsnag, Rollbar, Datadog — with its external id.

Something unclear or missing? Write to [email protected].