Webhooks

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:

SurfaceDirectionAuth
PR-trigger webhookGitHub → TerrantulaHMAC, per-action secret
GitHub App lifecycle webhookGitHub → TerrantulaHMAC, App secret
webhook triggerTerrantula → your endpointyou choose; callback uses Bearer
INFO

Inbound GitHub webhooks are HMAC-authenticated via X-Hub-Signature-256not 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).


PR-trigger webhook

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.

  1. GitHub POSTs a pull_request event.
  2. Only the closed action is significant; everything else is acknowledged with { ok: true }.
  3. Terrantula finds an active (pending/running) ActionRun whose metadata.repo matches repository.full_name. No match → silently acknowledged (not a Terrantula-managed PR).
  4. It resolves that run's webhookSecret and verifies the signature. Invalid signature → 401. No secret configured → acknowledged, no transition (HMAC skipped).
  5. On a verified merge → ActionRun 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.

NOTE

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.


GitHub App lifecycle webhook

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 / actionEffect
installation createdAcknowledged — the install-callback owns row creation.
installation deletedRow → deleted; cached installation tokens cleared.
installation suspend / unsuspendRow → suspended / active; tokens cleared.
installation_repositories added/removedUpdates cached granted_repos; emits an audit + notification.
pull_request closedTransitions 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 trigger

Purpose. 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.

trigger:
  type: webhook
  url: https://workflows.example.com/onboard
  method: POST                       # default POST; GET/PUT/PATCH/DELETE supported
  auth:
    type: bearer                     # bearer | basic | header | jwt
    token: "{{ secrets.api-token }}"
  headers:
    X-Tenant: "{{ parameters.tenant_id }}"
  payload:                           # JSON body, interpolated
    customer_id: "{{ entity.id }}"
    callback_url: "{{ run.callbackUrl }}"
    callback_token: "{{ run.callbackToken }}"
  retry:                             # default: 3 attempts, exponential, 1000ms
    maxAttempts: 3
    backoff: exponential             # exponential | fixed
    initialDelayMs: 1000

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.

WARNING

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).