Forward GitHub Webhooks to Discord, Slack, or Any Endpoint

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.

The Problem: GitHub's Format ≠ Your Format

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.

Setting Up Your GitHub Webhook Relay

The setup takes three API calls. No server to deploy, no code to write.

Step 1: Create an API Key

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..."
}

Step 2: Create a Hook with Transform Rules

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.

Forward GitHub Webhooks to Discord

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.

Forward GitHub Webhooks to Slack

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.

Forward to a Custom Endpoint

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"
}

Step 3: Configure GitHub

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:

  1. Go to your repository → SettingsWebhooksAdd webhook
  2. Set Payload URL to https://hookpipe.app/hooks/YOUR_HOOK_ID
  3. Set Content type to application/json
  4. Choose which events to send (or select "Send me everything" and use Hookpipe's filters to narrow it down)
  5. Click Add webhook

That'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.

Inspect and Replay Failed Deliveries

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.

Quick Reference: Common GitHub Event Transforms

Here are transform configs for common GitHub events you might want to forward:

Issues → Slack

{
  "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"}
  ]
}

Releases → Discord

{
  "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"}
  ]
}

Push Events → Custom API

{
  "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"
  }
}

Start Forwarding GitHub Webhooks in 60 Seconds

No server to deploy. No code to write. Just configure, point, and go.

Quick Start Guide → Open Dashboard