GitHub sends webhook events in its own format: deeply nested JSON with repository objects, sender info, and action metadata. But your destination expects something different. Discord wants an embeds array. Slack wants blocks or a simple text field. Your internal API expects a flat payload. Hookpipe sits in the middle. It receives GitHub's webhooks, transforms the payload into whatever your destination needs, and forwards it automatically. Automatic retry on failure is included.
When you configure a webhook in a GitHub repository, every event (pushes, pull requests, issues, releases) sends a POST request with a standardized but complex payload. Here's a simplified example of what GitHub sends when someone opens a pull request:
{
"action": "opened",
"number": 42,
"pull_request": {
"title": "Fix authentication bug in login flow",
"html_url": "https://github.com/acme/api/pull/42",
"user": { "login": "jane-dev", "avatar_url": "https://avatars.githubusercontent.com/u/12345" },
"base": { "ref": "main" },
"head": { "ref": "fix/auth-bug" }
},
"repository": {
"full_name": "acme/api",
"html_url": "https://github.com/acme/api"
},
"sender": { "login": "jane-dev" }
}
This is fine if your destination understands GitHub's schema. But most don't. Discord's incoming webhook API expects a completely different shape. Slack expects another. Your custom monitoring dashboard expects something else entirely.
Without a relay layer, you'd need to write a server that receives GitHub events, transforms the payload, and posts it to your destination. That means infrastructure, deployment, uptime monitoring, and error handling for what should be a simple translation.
Hookpipe is that relay layer. You configure the transform once, and it handles the rest. This includes automatic retry with exponential backoff if your destination is temporarily down.
The setup takes three API calls. No server to deploy, no code to write.
API keys let you manage your hooks. The key is returned once at creation — save it somewhere safe.
curl -X POST https://hookpipe.app/api/auth/keys \
-H "Content-Type: application/json" \
-d '{"name": "github-relay"}'
Response:
{
"id": "key_a1b2c3d4",
"name": "github-relay",
"key": "hp_live_abc123..."
}
This is where you define what comes in and what goes out. The hook creation request includes your destination URL and the transform configuration.
We'll walk through three destination examples: Discord, Slack, and a custom endpoint.
Discord incoming webhooks expect a JSON payload with a content field (plain text) or an embeds array for rich messages. Hookpipe's field mapping with dot notation lets you pull values from GitHub's nested payload and place them where Discord expects:
curl -X POST https://hookpipe.app/api/hooks \
-H "Authorization: Bearer hp_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "github-to-discord",
"destinationUrl": "https://discord.com/api/webhooks/1234567890/abcdefg",
"transformConfig": {
"fieldMapping": {
"content": "pull_request.title",
"embeds[0].title": "pull_request.title",
"embeds[0].url": "pull_request.html_url",
"embeds[0].description": "action",
"embeds[0].author.name": "sender.login",
"embeds[0].author.icon_url": "pull_request.user.avatar_url",
"username": "GitHub Bot"
},
"filters": [
{"field": "action", "operator": "equals", "value": "opened"}
]
}
}'
This hook only forwards events where action is "opened" — so you'll get notified when new PRs are opened, but not on every update, review, or label change. The username field makes the Discord message show as "GitHub Bot."
💡 Filtering saves noise
GitHub sends webhooks for dozens of actions on pull requests alone: opened, closed, synchronize, labeled, review_requested, and more. Use Hookpipe's filters to only forward the events you care about. Your Discord channel will thank you.
Slack incoming webhooks accept a text field for simple messages. For richer formatting, you can map fields into Slack's structure. Here's a straightforward setup that forwards push events:
curl -X POST https://hookpipe.app/api/hooks \
-H "Authorization: Bearer hp_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "github-to-slack",
"destinationUrl": "https://hooks.slack.com/services/T00000/B00000/XXXX",
"transformConfig": {
"fieldMapping": {
"text": "head_commit.message",
"channel": "repository.full_name",
"username": "GitHub",
"icon_emoji": ":octocat:"
},
"filters": [
{"field": "head_commit", "operator": "exists"}
]
}
}'
The filter {"field": "head_commit", "operator": "exists"} ensures this hook only fires for push events (which include a head_commit object), ignoring pull request events, issue events, and everything else.
The icon_emoji field gives your Slack messages the GitHub octocat icon, making it visually clear where the notification came from.
Not everything goes to chat. Maybe you need GitHub events in your monitoring system, a logging pipeline, or an internal API that expects a flat payload. Hookpipe's field mapping flattens GitHub's nested structure:
curl -X POST https://hookpipe.app/api/hooks \
-H "Authorization: Bearer hp_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "github-to-monitoring",
"destinationUrl": "https://api.internal.acme.com/events",
"transformConfig": {
"fieldMapping": {
"event_type": "action",
"repo": "repository.full_name",
"actor": "sender.login",
"pr_number": "number",
"pr_title": "pull_request.title",
"pr_url": "pull_request.html_url",
"target_branch": "pull_request.base.ref",
"source_branch": "pull_request.head.ref",
"source": "github",
"pipeline": "hookpipe"
}
}
}'
The incoming nested GitHub payload gets transformed into a clean, flat object:
GitHub sends →
{
"action": "opened",
"number": 42,
"pull_request": {
"title": "Fix auth bug",
"html_url": "https://...",
"base": { "ref": "main" },
"head": { "ref": "fix/auth" }
},
"repository": {
"full_name": "acme/api"
},
"sender": { "login": "jane-dev" }
}
→ Your API receives
{
"event_type": "opened",
"repo": "acme/api",
"actor": "jane-dev",
"pr_number": 42,
"pr_title": "Fix auth bug",
"pr_url": "https://...",
"target_branch": "main",
"source_branch": "fix/auth",
"source": "github",
"pipeline": "hookpipe"
}
Once your hook is created, Hookpipe returns a hook ID. Your webhook receive URL is:
https://hookpipe.app/hooks/YOUR_HOOK_ID
To set this up in GitHub:
https://hookpipe.app/hooks/YOUR_HOOK_IDapplication/jsonThat's it. GitHub will start sending events to Hookpipe, which transforms and forwards them to your configured destination. Every delivery is logged so you can inspect what arrived, what was transformed, and whether forwarding succeeded.
If your destination was temporarily down when a GitHub event came in, Hookpipe retries up to 3 times with exponential backoff. But if all retries fail, you don't lose the payload. Every webhook is stored for 7 days (30 days on Pro), and you can replay any delivery:
# List recent payloads for your hook
curl https://hookpipe.app/api/hooks/YOUR_HOOK_ID/payloads \
-H "Authorization: Bearer hp_live_abc123..."
# Replay a specific payload
curl -X POST https://hookpipe.app/api/hooks/YOUR_HOOK_ID/payloads/PAYLOAD_ID/replay \
-H "Authorization: Bearer hp_live_abc123..."
Replayed deliveries include an X-Replay: true header so your destination can distinguish them from original events if needed.
Here are transform configs for common GitHub events you might want to forward:
{
"fieldMapping": {
"text": "issue.title",
"attachments[0].title": "issue.title",
"attachments[0].title_link": "issue.html_url",
"attachments[0].text": "issue.body",
"username": "GitHub Issues",
"icon_emoji": ":bug:"
},
"filters": [
{"field": "action", "operator": "equals", "value": "opened"},
{"field": "issue", "operator": "exists"}
]
}
{
"fieldMapping": {
"content": "release.tag_name",
"embeds[0].title": "release.name",
"embeds[0].url": "release.html_url",
"embeds[0].description": "release.body",
"username": "GitHub Releases"
},
"filters": [
{"field": "action", "operator": "equals", "value": "published"}
]
}
{
"fieldMapping": {
"repo": "repository.full_name",
"branch": "ref",
"pusher": "pusher.name",
"commit_message": "head_commit.message",
"commit_url": "head_commit.url",
"commit_count": "commits.length",
"event": "push",
"source": "github"
}
}
No server to deploy. No code to write. Just configure, point, and go.