TILLFORGE · EVENTS & WEBHOOKS

When something happens, machines get a webhook — people get a notification.

GA

Every meaningful change in a repo becomes an event. Subscribe an endpoint and it arrives as a signed webhook you can verify and act on; the people who care get a personal notification in the dashboard. One thing happens; the right systems and the right humans both find out.

01Events

What fires

These are the events you can subscribe to. Each carries a small JSON body describing what changed and which repo it belongs to.

EventFires when
pushCommits are pushed to a branch.
pull_requestA pull request is opened or merged.
issueAn issue is opened.
releaseA release is published.
deploymentA deployment changes state.
checkA commit status / check is reported.
merge_queueThe merge queue starts testing a merge result (action: "testing", with trial_ref and trial_sha for your CI to fetch and report checks on), or an entry under test leaves the queue (action: "failed", with the reason).
pingYou send a test from the dashboard or CLI.
This is the glue for your pipeline
A push can kick off your CI; a published release can trigger a rollout; a deployment can update your status page. Run your jobs on TillForge runners, or let these events tell the CI you already have exactly when to run.
02Subscribe

Point an endpoint at the events

Add a webhook for an org (all repos) or a single repo, choose your events, and set a shared secret — the secret is what lets you prove a delivery is genuinely from TillForge. Send yourself a ping to confirm the endpoint is wired before anything real depends on it.

bash
# Subscribe an endpoint to the events you care about. It prints the
# signing secret your receiver verifies deliveries with, once.
tilldev forge webhook add \
  --url https://ci.example.com/hooks/tillforge \
  --events push,pull_request,release,deployment \
  --repo acme-api

tilldev forge webhook secret <id>        # replace it (or --secret <yours>)
tilldev forge webhook ls
tilldev forge webhook ping <id>          # send a test 'ping' event now
tilldev forge webhook deliveries <id>    # the recent delivery log
tilldev forge webhook rm <id>
03Verify

Verify every delivery

X-TillForge-Event names the event and X-TillForge-Delivery the delivery, which the body repeats as delivery_id alongside attempt. When the webhook has a secret, X-TillForge-Webhook-Signature carries t=<unix>,v1=<hex>: an HMAC-SHA256 of t.raw_body keyed by your secret, the same scheme as TillAuth webhooks. Recompute it over the exact bytes you received, compare in constant time, and refuse a t more than five minutes off. That’s how you know a request is really from us, untampered, and not a recording played back later. A retry keeps its delivery_id, so skip one you’ve already handled.

X-TillForge-Signature: sha256=<hex> (the body alone, as GitHub signs) is still sent for receivers written before. It proves origin but not freshness, so a captured delivery verifies forever; move to the timestamped header.

verify-webhook.ts
import crypto from 'node:crypto'

// Verify a delivery before you trust it. v1 is an HMAC-SHA256 of
// "<t>.<raw body>", keyed by your webhook secret.
export function isValid(rawBody: string, header: string, secret: string, now = Date.now() / 1000) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)))
  const t = Number(parts.t)
  if (!Number.isInteger(t) || Math.abs(now - t) > 300) return false // stale: a replay
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
  const got = Buffer.from(String(parts.v1 ?? ''), 'hex')
  // constant-time compare, never a plain ===
  return got.length === expected.length && crypto.timingSafeEqual(got, expected)
}

// isValid(rawBody, req.headers['x-tillforge-webhook-signature'], YOUR_SECRET)
// then skip a delivery_id you have already handled: retries reuse it.
Verify against the raw body
Compute the signature over the exact bytes you received, before any JSON parse or re-serialize — re-encoding can change a byte and break the match. And always compare with a constant-time check, never ===.
04Delivery

Delivery you can inspect

A delivery that fails is tried again: six attempts in all, at once and then after 1 minute, 5 minutes, 25 minutes, 2 hours and 10 hours. Every attempt logs its response and timing, viewable with forge webhook deliveries or on the Webhooks screen — so a receiver that was down is visible, not silent. Delivery is time-boxed, and a receiver that answers slowly never holds up the push, merge, or release that triggered it.

Where a webhook may point
For your protection, a webhook targets a public HTTPS endpoint you control — deliveries won’t follow redirects or reach internal network addresses. Point it at the service that should receive the event, not at a hop in between.
05Notifications

Notifications — for people, not machines

The human side of the same events. When your work gets attention, a notification lands in the dashboard’s Notifications screen, deep-linked to the thread and tracked as unread until you open it.

You get one whenOn
Review — someone reviews itYour pull request
Comment — someone repliesYour pull request or issue
Merge — it landsYour pull request

Notifications are personal — you only ever see your own, and you can only mark your own read. They’re a lightweight inbox for what needs your eyes; the durable, org-wide record of who did what lives in the audit log.


Next: Deployments and Releases & builds for the events worth wiring up, or the CLI reference. Back to the TillForge overview.