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:
- Navigate to your repository
- Go to Settings → Hooks
- Click new webhook
- 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 openedpull_request:resubmitted— a new round (revision) was pushed to a pull requestpull_request:merged— a pull request was mergedpull_request:closed— a pull request was closedpull_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:
actionmirrors the event type suffix (created,resubmitted,merged,closed,reopened).repositoryhas 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_urlinstead.round_numberidentifies the latest round, andpatch_urlalways points at that round’s patch. sourceis only present for branch-based and fork-based pull requests; it is omitted for patch-based pulls. For fork-based pulls,source.repocontains the DID of the source repository.senderis the user who performed the action.
HTTP headers
Each webhook request includes the following headers:
Content-Type: application/jsonUser-Agent: Tangled-Hook/<short-sha>— User agent with short SHA of the commit (push events);Tangled-Hook/pull_requestfor pull request eventsX-Tangled-Event: push— The full event type (e.g.push,pull_request:merged)X-Tangled-Hook-ID: <webhook-id>— The webhook IDX-Tangled-Delivery: <uuid>— Unique delivery IDX-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:
- Respond with 200 OK immediately
- 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");
});