View as single-page

Webhooks

Webhooks allow you to receive HTTP POST notifications when events occur in your repositories. This enables you to integrate Tangled with external services, trigger CI/CD pipelines, send notifications, or automate workflows.

Overview

Webhooks send HTTP POST requests to URLs you configure whenever specific events happen. Currently, Tangled supports push, repository rename, and pull request events, with more event types coming soon.

Configuring webhooks

To set up a webhook for your repository:

  1. Navigate to your repository
  2. Go to Settings → Hooks
  3. Click new webhook
  4. Configure your webhook:
    • Payload URL: The endpoint that will receive the webhook POST requests
    • Secret: An optional secret key for verifying webhook authenticity (leave blank to send unsigned webhooks)
    • Events: Select which events trigger the webhook
    • Active: Toggle whether the webhook is enabled

Webhook payload

Push

When a push event occurs, Tangled sends a POST request with a JSON payload of the format:

{
  "after": "7b320e5cbee2734071e4310c1d9ae401d8f6cab5",
  "before": "c04ddf64eddc90e4e2a9846ba3b43e67a0e2865e",
  "pusher": { 
    "did": "did:plc:hwevmowznbiukdf6uk5dwrrq" 
  },
  "ref": "refs/heads/main",
  "repository": {
    "clone_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo",
    "created_at": "2025-09-15T08:57:23Z",
    "description": "an example repository",
    "fork": false,
    "full_name": "did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo",
    "html_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo",
    "name": "some-repo",
    "open_issues_count": 5,
    "owner": { 
      "did": "did:plc:hwevmowznbiukdf6uk5dwrrq" 
    },
    "ssh_url": "ssh://[email protected]/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo",
    "stars_count": 1,
    "updated_at": "2025-09-15T08:57:23Z"
  }
}

Pull request

Pull request events are sent as separate event types, so you can subscribe to exactly the transitions you care about:

  • pull_request:created — a pull request was opened
  • pull_request:resubmitted — a new round (revision) was pushed to a pull request
  • pull_request:merged — a pull request was merged
  • pull_request:closed — a pull request was closed
  • pull_request:reopened — a closed pull request was reopened

All pull request events share the same payload format:

{
  "action": "created",
  "pull_request": {
    "number": 4,
    "title": "add dark mode",
    "body": "implements dark mode as discussed in #2",
    "state": "open",
    "target_branch": "main",
    "source": {
      "branch": "dark-mode",
      "sha": "7b320e5cbee2734071e4310c1d9ae401d8f6cab5"
    },
    "round_number": 0,
    "owner": {
      "did": "did:plc:hwevmowznbiukdf6uk5dwrrq"
    },
    "html_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo/pulls/4",
    "patch_url": "https://tangled.org/did:plc:hwevmowznbiukdf6uk5dwrrq/some-repo/pulls/4/round/0.patch",
    "created_at": "2025-09-15T08:57:23Z"
  },
  "repository": { ... },
  "sender": {
    "did": "did:plc:hwevmowznbiukdf6uk5dwrrq"
  }
}

Notes:

  • action mirrors the event type suffix (created, resubmitted, merged, closed, reopened).
  • repository has the same format as in the push payload.
  • The patch itself is not embedded in the payload (patches can be large); fetch it from patch_url instead. round_number identifies the latest round, and patch_url always points at that round’s patch.
  • source is only present for branch-based and fork-based pull requests; it is omitted for patch-based pulls. For fork-based pulls, source.repo contains the DID of the source repository.
  • sender is the user who performed the action.

HTTP headers

Each webhook request includes the following headers:

  • Content-Type: application/json
  • User-Agent: Tangled-Hook/<short-sha> — User agent with short SHA of the commit (push events); Tangled-Hook/pull_request for pull request events
  • X-Tangled-Event: push — The full event type (e.g. push, pull_request:merged)
  • X-Tangled-Hook-ID: <webhook-id> — The webhook ID
  • X-Tangled-Delivery: <uuid> — Unique delivery ID
  • X-Tangled-Signature-256: sha256=<hmac> — HMAC-SHA256 signature (if secret configured)

Verifying webhook signatures

If you configured a secret, you should verify the webhook signature to ensure requests are authentic. For example, in Go:

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
    "strings"
)

func verifySignature(payload []byte, signatureHeader, secret string) bool {
    // Remove 'sha256=' prefix from signature header
    signature := strings.TrimPrefix(signatureHeader, "sha256=")
    
    // Compute expected signature
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := hex.EncodeToString(mac.Sum(nil))
    
    // Use constant-time comparison to prevent timing attacks
    return hmac.Equal([]byte(signature), []byte(expected))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    // Read the request body
    payload, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }
    
    // Get signature from header
    signatureHeader := r.Header.Get("X-Tangled-Signature-256")
    
    // Verify signature
    if signatureHeader != "" && verifySignature(payload, signatureHeader, yourSecret) {
        // Webhook is authentic, process it
        processWebhook(payload)
        w.WriteHeader(http.StatusOK)
    } else {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
    }
}

Delivery retries

Webhooks are automatically retried on failure:

  • 3 total attempts (1 initial + 2 retries)
  • Exponential backoff starting at 1 second, max 10 seconds
  • Retried on:
    • Network errors
    • HTTP 5xx server errors
  • Not retried on:
    • HTTP 4xx client errors (bad request, unauthorized, etc.)

Timeouts

Webhook requests timeout after 30 seconds. If your endpoint needs more time:

  1. Respond with 200 OK immediately
  2. Process the webhook asynchronously in the background

Example integrations

Discord notifications

app.post("/webhook", (req, res) => {
  const payload = req.body;

  fetch("https://discord.com/api/webhooks/...", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      content: `New push to ${payload.repository.full_name}`,
      embeds: [
        {
          title: `${payload.pusher.did} pushed to ${payload.ref}`,
          url: payload.repository.html_url,
          color: 0x00ff00,
        },
      ],
    }),
  });

  res.status(200).send("OK");
});