Webhooks are how external systems report back to Terrantula and how the
generic webhook trigger reports out. There are three
distinct surfaces, each with its own auth model:
| Surface | Direction | Auth |
|---|---|---|
| PR-trigger webhook | GitHub → Terrantula | HMAC, per-action secret |
| GitHub App lifecycle webhook | GitHub → Terrantula | HMAC, App secret |
webhook trigger | Terrantula → your endpoint | you choose; callback uses Bearer |
Inbound GitHub webhooks are HMAC-authenticated via X-Hub-Signature-256 —
not a Bearer token. They self-authenticate inside the route handler and
bypass project auth + RBAC. The callback that the generic webhook trigger
hands outto your workflow is the one that uses a Bearer token (a short-lived,
per-run callback token).
Route. POST /:org/:project/webhooks/github (project-scoped).
Purpose. Auto-complete a pull-request
(or Atlantis pull-request-mode) ActionRun when its PR is merged or closed.
Auth. HMAC. Terrantula verifies X-Hub-Signature-256 against the per-action
webhook secret — the secret named by the trigger's webhookSecret field
(e.g. {{ secrets.github-webhook-secret }}), resolved from Terrantula
Secrets scoped to the matched run's
(projectId, envId). There is no global webhook configuration: different
repos can use different secrets without coordination, and two environments of the
same project can sign with different secrets under the same name.
Flow.
pull_request event.closed action is significant; everything else is acknowledged with
{ ok: true }.pending/running) ActionRun whose
metadata.repo matches repository.full_name. No match → silently
acknowledged (not a Terrantula-managed PR).webhookSecret and verifies the signature. Invalid
signature → 401. No secret configured → acknowledged, no transition (HMAC
skipped).succeeded; closed without merge → failed.
The entity/relationship transitions to onSuccess/onFailure.Configure on GitHub. Add a repo webhook (or use the
GitHub App) pointing at
POST /:org/:project/webhooks/github, content type application/json, with the
same secret you reference from the action's webhookSecret.
If webhookSecret is omitted, the PR still opens — but the run won't
auto-complete. You then transition it manually via the entity/relationship API,
or have a GitHub Actions workflow POST the run's callback URL. Embed
{{ run.id }} (the ActionRun UUID) in the PR body for reference. If neither
happens, the reaper times the run out after the Action's timeout.
Route. POST /webhooks/github (global; mounted only when
TERRANTULA_DEPLOYMENT=cloud).
Purpose. Drive GitHub App lifecycle: installation created/deleted/suspended/unsuspended and repository-grant changes.
Auth. HMAC. Terrantula verifies X-Hub-Signature-256 against the App-level
GITHUB_APP_WEBHOOK_SECRET. Invalid signature → 401 (logged with the event
name + installation id, no payload). Unconfigured App → 503.
Events handled.
| Event / action | Effect |
|---|---|
installation created | Acknowledged — the install-callback owns row creation. |
installation deleted | Row → deleted; cached installation tokens cleared. |
installation suspend / unsuspend | Row → suspended / active; tokens cleared. |
installation_repositories added/removed | Updates cached granted_repos; emits an audit + notification. |
pull_request closed | Transitions the matching PR-trigger run (same as the per-action path, matched via project_github_repos or legacy (repo, prNumber) metadata). |
Unrecognized events return { ok: true, outcome: { kind: 'event-ignored' } }. If
an audit/notification insert throws, the route returns 500 so GitHub retries
the delivery.
webhook triggerPurpose. The outbound generic trigger — call any HTTP endpoint with an interpolated payload, then let your workflow report completion via the run's callback URL. Use it when no first-class substrate trigger fits.
Config. See the Action schema → triggers for the full field table.
Outbound auth types. bearer (Authorization: Bearer …), basic
(Authorization: Basic base64(user:pass)), header (a custom header
name+value), jwt (signs a JWT with the named secret).
Completion (the Bearer callback). Your workflow POSTs to
{{ run.callbackUrl }} with Authorization: Bearer {{ run.callbackToken }} and
a JSON body indicating success or failure. The ActionRun transitions to
succeeded/failed and the entity to onSuccess/onFailure. Both values are
available in the run interpolation context.
Retry behavior. Retries on 5xx, 429, and network errors with exponential
backoff + jitter. 4xx (except 429) fails immediately — retrying a misconfigured
URL won't help. Delivery failures carry a [delivery] prefix on the ActionRun's
error field, distinguishing them from callback-reported failures.
Never embed long-lived secrets in payload. Use {{ secrets.* }} references
(resolved server-side) and the short-lived {{ run.callbackToken }}for the
return path. The callback token is per-run and expires with the run.
If the callback never arrives, the reaper transitions the run to failed after
the Action's timeout (default 60 minutes).
{{ secrets.* }} resolutionrun.callbackUrl / run.callbackToken