Skip to content

Custom Webhook: deliver to any endpoint

The Custom Webhook integration delivers every share you create as a structured, HMAC-signed JSON payload to any public https URL you own — automation platforms (n8n, Zapier, Make, IFTTT), your own backend, ticketing systems, or a bot relay.

One URL is all it takes: no app registration, no credentials to obtain. A server-generated HMAC secret lets your consumer verify that requests really come from BugCapturer.

Step 1: Get a Webhook URL from your target

Section titled “Step 1: Get a Webhook URL from your target”

Create a Webhook trigger/receiving node in your target system and copy its https URL:

  • n8n: add a Webhook node → use the Production URL (method: POST)
  • Zapier: build a Zap with the Catch Hook trigger and copy the hook URL
  • Make: add a Custom webhook module and copy its URL
  • Your own system: expose any public https endpoint that accepts POST with JSON

Step 2: Add the integration in BugCapturer

Section titled “Step 2: Add the integration in BugCapturer”
  1. Sign in at app.bugcapturer.com and open the “Integrations” page
  2. Select “Custom Webhook” in the left platform list → click “Add integration”
  3. Fill in a name and the Webhook URL (https, public)
  4. Click “Verify & save” — the backend validates the URL (public https only) and sends a signed test event (event=test) to your endpoint
  5. When the test succeeds, the HMAC secret is shown once — copy it into your consumer’s configuration immediately. Only the last 4 characters are shown afterwards; you can rotate it anytime via the card menu → “Rotate secret”

Every share triggers a POST request with Content-Type: application/json (UTF-8), payload schema v1:

{
"event": "share.created",
"source": "BugCapturer",
"schema_version": 1,
"sent_at": "2026-09-14T21:30:00+08:00",
"share": {
"token": "a1b2c3d4",
"report_url": "https://app.bugcapturer.com/share/a1b2c3d4",
"screenshot_url": "https://api.bugcapturer.com/api/shares/a1b2c3d4/file",
"submitted_at": "2026-09-14T21:29:58+08:00",
"expires_at": "2026-12-13T21:29:58+08:00"
},
"report": {
"description": "Submit-order button does not respond",
"feedback_type": "",
"page_url": "https://example.com/checkout",
"page_title": "Checkout - Example Shop",
"browser": "Chrome 128",
"os": "Windows 11",
"screen": "1920x1080",
"console_errors_count": 2,
"network_errors_count": 0
}
}
FieldMeaning
eventshare.created (a share was created) or test (save check / “Test” button)
schema_versionAlways 1; bumped only on breaking changes, so consumers can stay compatible
share.tokenUnique share token
share.report_urlFull report page (opens in browser, link does not expire)
share.screenshot_urlScreenshot proxy URL (302 to a signed storage URL)
share.expires_atWhen the share expires and its files are deleted
report.descriptionProblem description, truncated at 2000 chars
report.page_urlSanitized URL of the page where the report was captured
report.feedback_typeReserved — always empty in schema v1 (the extension form no longer collects a report type)
report.*_errors_countCounts only — for privacy, console/network error contents are never included; open report_url for the full details
all timestampsISO 8601 with +08:00 offset

Every request carries the header:

X-BugCapturer-Signature: t=<unix timestamp>,v1=<hex digest>

where v1 = HMAC-SHA256(secret, "{t}.{raw_request_body}"). To verify, recompute the HMAC over "{t}." + raw body and compare with a constant-time equality check. Also reject timestamps deviating more than 5 minutes from your clock (replay protection).

Python:

import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > tolerance:
return False # outside clock tolerance — possible replay
expected = hmac.new(secret.encode(),
f'{parts["t"]}.'.encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
# Flask example
@app.route("/webhooks/bugcapturer", methods=["POST"])
def hook():
if not verify(SECRET, request.headers["X-BugCapturer-Signature"], request.get_data()):
return "bad signature", 401
payload = request.get_json()
... # route into your workflow
return "", 204

Node.js:

const crypto = require("crypto");
function verify(secret, header, rawBody, tolerance = 300_000) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() - Number(parts.t) * 1000) > tolerance) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`, "utf8")
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
// Express example (needs the raw body)
app.post("/webhooks/bugcapturer",
express.raw({ type: "application/json" }),
(req, res) => {
if (!verify(SECRET, req.get("X-BugCapturer-Signature"), req.body)) {
return res.status(401).send("bad signature");
}
const payload = JSON.parse(req.body);
... // route into your workflow
res.status(204).end();
});
  • Shown once after creation (and after rotation); the card afterwards shows only ······<last 4>
  • Rotate secret (card menu) revokes the old secret immediately — update your consumer, then click “Test” to confirm
  • The secret never appears in delivery logs, error messages, or emails

“URL not valid” on save? Only public https URLs are accepted (no http://, no localhost/private IPs, no userinfo in the URL). This protects our network from SSRF.

Test delivery fails with 401/403? Your endpoint requires its own authentication. Either allow requests carrying the X-BugCapturer-Signature header, or put an auth token in the URL path accepted by your system (n8n / Zapier hook URLs already embed one).

Can one report go to several integrations? Yes — configure multiple integrations, then pick the target with the “Sync target” selector in the extension before sharing. Only the selected integration receives that report.