> ## Documentation Index
> Fetch the complete documentation index at: https://docs.roboticks.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> The GitHub webhook events Roboticks subscribes to, how we verify them with HMAC, and how replay protection prevents stale deliveries from triggering jobs.

# Webhooks

Every action GitHub takes that matters to Roboticks — a PR opened, a push to `main`, an installation suspended — arrives as a webhook delivery to `https://api.roboticks.io/api/v1/github/webhooks`. This page documents what we listen to, how we verify it, and what happens next.

## Endpoint

```
POST https://api.roboticks.io/api/v1/github/webhooks
Content-Type: application/json
X-GitHub-Event: <event>
X-GitHub-Delivery: <uuid>
X-Hub-Signature-256: sha256=<hmac>
```

The endpoint is public, idempotent on `X-GitHub-Delivery`, and returns within 1 second for every delivery (heavy work is queued).

## Subscribed events

| Event                       | Sub-actions handled                                               | What we do                                                                                                                           |
| --------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `installation`              | `created`, `deleted`, `suspend`, `unsuspend`                      | Create / disable / re-enable the installation record. On `deleted`, retain history; on `suspend`, stop dispatching new jobs.         |
| `installation_repositories` | `added`, `removed`                                                | Sync the project ↔ repo mapping. Removed repos go to a `disconnected` state but historical data is retained.                         |
| `pull_request`              | `opened`, `synchronize`, `reopened`, `ready_for_review`, `closed` | Enqueue a test run for the head SHA. On `closed` we cancel any in-flight job for that PR.                                            |
| `push`                      | `created`, `deleted` (branch)                                     | Trigger runs for branches that match the project's branch policy — by default `main`, `master`, and `release/*`.                     |
| `workflow_run`              | `completed`                                                       | If a customer runs the Roboticks SDK inside their own GitHub Actions workflow, we pick up the JUnit + MCAP artifacts via this event. |
| `check_suite`               | `rerequested`, `requested`                                        | Re-dispatch the test run when a user clicks **Re-run all jobs** on the Check Suite.                                                  |

Everything else is **ignored at the load balancer** — we don't even parse the body.

## What happens on a delivery

```mermaid theme={null}
%%{init: {"theme": "neutral", "themeVariables": {"primaryColor": "#4040ff"}} }%%
sequenceDiagram
    participant GH as GitHub
    participant API as api.roboticks.io
    participant Q as Job queue
    participant W as Worker
    participant Run as Runner
    GH->>API: POST /github/webhooks
    API->>API: HMAC verify + dedup on X-GitHub-Delivery
    API->>Q: Enqueue {event, payload}
    API-->>GH: 202 Accepted
    Q->>W: Pop
    W->>W: Resolve installation, project, repo
    W->>Run: Dispatch job (or skip if no test scope)
```

The HTTP response is **always within 1 s** because we acknowledge before processing. GitHub retries on 5xx; we do not want to be in the retry loop, so heavy work is queued behind a fast ack.

## HMAC verification

Each installation has a webhook secret rotated every 90 days. GitHub signs every delivery with that secret using HMAC-SHA256 over the raw request body. The signature arrives in `X-Hub-Signature-256`.

```python theme={null}
import hmac, hashlib

def verify(raw_body: bytes, header_sig: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header_sig)
```

Three things to know:

1. **We verify on the raw body**, before JSON parsing. Reformatting the body invalidates the signature.
2. We use `hmac.compare_digest` for **constant-time comparison** — no timing side channels.
3. Any failure short-circuits to **HTTP 401** and the delivery is dropped. GitHub will retry; if your secret has actually rotated and our copy is stale, the retries will all fail and you'll see the corresponding alert on the dashboard.

<Warning>
  The webhook secret is the only thing that proves a delivery came from GitHub. Do not log it. Do not put it in error messages. If you suspect it's leaked, rotate it from **Settings → Integrations → GitHub App → Rotate webhook secret** — GitHub re-signs subsequent deliveries automatically.
</Warning>

## Replay protection

`X-GitHub-Delivery` is a UUID per delivery. We store every delivery ID for **24 hours** in Redis and 401 on any duplicate. Why:

* A leaked-but-valid signed payload can't be replayed to re-trigger a job.
* A malicious mirror that captures and re-posts cannot bypass the window.

24 hours is well above the GitHub retry window (8 attempts over \~8 hours). Outside the window, the SHA-bound idempotency keys on the job queue catch duplicates as well — replays cannot create duplicate jobs.

## Payload examples

<AccordionGroup>
  <Accordion title="pull_request.opened (excerpt)">
    ```json theme={null}
    {
      "action": "opened",
      "number": 142,
      "pull_request": {
        "head": { "sha": "abcd1234...", "ref": "feature/estop-deadline" },
        "base": { "sha": "9876fedc...", "ref": "main" },
        "draft": false
      },
      "repository": {
        "full_name": "acme-robotics/amr-stack"
      },
      "installation": { "id": 12345678 }
    }
    ```
  </Accordion>

  <Accordion title="installation.created (excerpt)">
    ```json theme={null}
    {
      "action": "created",
      "installation": {
        "id": 12345678,
        "account": { "login": "acme-robotics", "type": "Organization" },
        "repository_selection": "selected"
      },
      "repositories": [
        { "full_name": "acme-robotics/amr-stack" }
      ]
    }
    ```
  </Accordion>

  <Accordion title="check_suite.rerequested (excerpt)">
    ```json theme={null}
    {
      "action": "rerequested",
      "check_suite": {
        "head_sha": "abcd1234...",
        "head_branch": "feature/estop-deadline"
      },
      "repository": { "full_name": "acme-robotics/amr-stack" },
      "installation": { "id": 12345678 }
    }
    ```
  </Accordion>
</AccordionGroup>

## Delivery observability

Two surfaces:

* **GitHub side** — `Settings → Applications → Roboticks → Advanced → Recent Deliveries`. Every delivery, its payload, our response code, and a redeliver button.
* **Roboticks side** — `Settings → Integrations → GitHub App → Webhook log`. Same data, plus the resolved installation, project, and downstream job ID for each delivery.

Both surfaces are kept for **30 days**.

## Skipped deliveries

We accept the delivery (202) but skip downstream processing if:

* The installation is **suspended**.
* The repo has been removed from the installation since the event fired.
* The repo is not linked to any Roboticks project (this is normal during onboarding).
* The PR is a draft and the project's policy excludes drafts (default: skip drafts).

Skipped deliveries are visible in the webhook log with reason codes.

## Next

<CardGroup cols={2}>
  <Card title="Permissions" icon="lock" href="/github-app/permissions">
    The scopes that back the webhook handler.
  </Card>

  <Card title="Check Runs" icon="circle-check" href="/github-app/check-runs">
    The output produced by a webhook-triggered job.
  </Card>

  <Card title="Troubleshooting" icon="bug" href="/github-app/troubleshooting">
    Webhook 4xx errors and how to recover.
  </Card>
</CardGroup>
