Skip to main content

Webhooks

Outbound webhooks let you route Roboticks events into anything that accepts HTTP — internal dashboards, ticketing systems, data warehouses, custom Slack apps your security team has standardised on, or your own tooling.

Set up

1

Add a webhook

Settings → Integrations → Webhooks → Add. Paste your endpoint URL (HTTPS only). Pick a name.
2

Generate a signing secret

Roboticks generates a 32-byte secret. Copy it once — it’s never shown in plaintext again. Store it where your receiver can read it.
3

Pick events

Toggle which event types route here. Defaults: all test_run.* and evidence_pack.* events.
4

Send a test

Hit Send test event. The endpoint receives a synthetic test_run.completed payload.

Payload envelope

Every webhook delivery has the same envelope:
{
  "id": "evt_7c2f...",
  "type": "test_run.completed",
  "occurred_at": "2026-05-24T12:44:15.221Z",
  "org": { "id": 42, "slug": "acme" },
  "project": { "id": 18, "slug": "warehouse" },
  "data": { /* event-type-specific payload, see below */ },
  "links": {
    "dashboard": "https://app.roboticks.io/r/warehouse/runs/8a1f3c2d"
  }
}

Event types

test_run.completed

"data": {
  "run_id": "8a1f3c2d-...",
  "status": "failed",
  "git": { "sha": "abcdef123456", "ref": "refs/heads/main", "pr_number": 214 },
  "pool": { "name": "prod-gpu-farm", "type": "self-hosted" },
  "tests": { "total": 412, "passed": 411, "failed": 1, "skipped": 0 },
  "duration_seconds": 139,
  "failures": [{
    "id": "tests/test_estop.py::test_estop_halts_motion",
    "message": "stop reached at 142ms (budget 100ms)",
    "requirements": ["REQ-001"]
  }],
  "requirement_coverage": {
    "confirmed": 142,
    "uncovered": 18,
    "total": 160,
    "delta_vs_previous": { "confirmed": -1, "uncovered": 1, "stale": 0 }
  }
}

requirement.gap_opened

"data": {
  "requirement_id": "REQ-051",
  "title": "Tool-change interlock",
  "type": "safety",
  "previously_confirmed_by": "tests/test_toolchange.py::test_interlock",
  "last_confirmed_run_id": "8a1a72e0-...",
  "last_confirmed_at": "2026-05-21T17:30:00Z",
  "reason": "test_deleted",
  "responsible_commit": { "sha": "abcdef123456", "author": "amir@roboticks.io" }
}
reason is one of: test_deleted, test_renamed, test_failing, requirement_added, staleness_window_exceeded.

evidence_pack.generated

"data": {
  "pack_id": "ev_a1b2...",
  "release_tag": "v2.4.0",
  "size_bytes": 4423012,
  "formats": ["pdf", "reqif", "zip"],
  "hash_chain_entries": 142,
  "signature": {
    "algorithm": "cosign",
    "key_id": "rbtk-prod-2026"
  },
  "download_urls": {
    "pdf":   "https://api.roboticks.io/v1/evidence-packs/ev_a1b2/pdf",
    "reqif": "https://api.roboticks.io/v1/evidence-packs/ev_a1b2/reqif",
    "zip":   "https://api.roboticks.io/v1/evidence-packs/ev_a1b2/zip"
  }
}

standard.amendment_published

"data": {
  "standard": "iso-10218-2-2025",
  "amendment": "A1",
  "published_at": "2026-04-12",
  "summary": "Revised §5.7.5 protective-stop performance criteria",
  "impacted_pinned_requirements": ["REQ-001", "REQ-009", "REQ-049"],
  "change_impact_report_url": "https://api.roboticks.io/v1/standards/iso-10218-2-2025/A1/impact"
}

runner_pool.offline

"data": {
  "pool_name": "onprem-airgapped",
  "pool_type": "self-hosted",
  "airgapped": true,
  "last_online_at": "2026-05-24T11:27:01Z",
  "queued_jobs": 14
}

Signatures

Every delivery includes an HMAC-SHA256 signature in the X-Roboticks-Signature header:
X-Roboticks-Signature: t=1716561855,v1=abc123...
t is the Unix timestamp the signature was minted (use to defeat replay); v1 is the HMAC.

Verify

Compute HMAC-SHA256 over "{t}.{raw_body}" with your signing secret. Compare to v1 in constant time.
import hmac, hashlib, time

def verify(raw_body: bytes, sig_header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in sig_header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > tolerance:
        return False  # outside replay window
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
Always reject deliveries outside the 5-minute timestamp tolerance to defeat replay attacks.

Retry semantics

Failed deliveries (non-2xx response, timeout > 10 s, connection error) retry with exponential backoff:
AttemptDelay
1immediate
230 s
35 min
430 min
54 h
624 h
After 6 failed attempts, the delivery is marked failed and a webhook.delivery_failed event fires (you can wire that to Slack to know you’re losing data). Replays from the dashboard: Settings → Integrations → Webhooks → Delivery log → ⋯ → Resend.

Idempotency

Use the envelope id for idempotency. The same event will never appear under two different id values; the same id may appear more than once if your endpoint timed out and we retried.
INSERT INTO webhook_events (id, type, occurred_at, payload)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO NOTHING;

Best practice

  • Acknowledge fast: return 2xx within 5 s, then process async. Don’t do heavy work in the request handler.
  • Verify the signature before parsing JSON.
  • Log the envelope id for every delivery; correlate with the delivery log on triage.
  • Reject deliveries whose org.id doesn’t match your expectation (defence-in-depth).

Troubleshooting

Most often the endpoint takes > 10 s to respond. Move processing to a queue and ack immediately.
You’re parsing JSON before computing the HMAC. Compute the HMAC over the raw bytes of the request body — even whitespace differences invalidate the signature.
Use webhook.site or ngrok http 4000 for local development. Both surface the exact headers and body Roboticks sends.