---
title: Webhooks
description: Receive HTTP POST notifications when events occur in your repositories.
---


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:

```json
{
  "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://git@tangled.org/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:

```json
{
  "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:

```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

```javascript
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");
});
```

