Skip to main content

GitHub Webhooks → Discord

Category: Engineering & DevOps · 📦 1 install

Get this pack →

This page is generated from the Air Pipe marketplace. Browse it live to install into your organization.

Forwards GitHub events (push, pull request, issues) to a Discord channel as formatted embeds. No database required — it's a pure HTTP passthrough. Set up in under 5 minutes with two free accounts.


What's included

FilePurpose
config.ymlAirPipe config — push, PR, and issues endpoints
schema.sqlOptional Postgres table for event logging

Endpoints

MethodPathGitHub event to select
POST/github/pushPush
POST/github/pull-requestPull requests
POST/github/issuesIssues

Each endpoint is independent. Configure only the events you care about.


Setup

1. Get a Discord webhook URL

  1. Open your Discord server → right-click the channel → Edit Channel
  2. Go to IntegrationsWebhooksNew Webhook
  3. Copy the webhook URL — it looks like:
    https://discord.com/api/webhooks/1234567890/AbCdEfGhIjKl...

2. Set the managed variables

In the AirPipe dashboard, add two managed variables:

NameValue
DISCORD_WEBHOOK_URLhttps://discord.com/api/webhooks/your-id/your-token
GITHUB_WEBHOOK_SECRETthe secret you set when creating the GitHub webhook

3. Deploy the config

Point AirPipe at config.yml. In hosted mode, upload via the dashboard or CLI.

4. Add GitHub webhooks

For each event type you want, go to your GitHub repo:
Settings → Webhooks → Add webhook

FieldValue
Payload URLhttps://your-airpipe-host/github/push
Content typeapplication/json ⚠️ must be JSON, not application/x-www-form-urlencoded
Which eventsSelect the matching event (Push / Pull requests / Issues)

Repeat for each endpoint you're enabling.

Important: GitHub's default content type is application/x-www-form-urlencoded. The config expects application/json — make sure to change this in the dropdown or body parsing will fail.


Testing

Simulate a push event:

curl -X POST https://your-airpipe-host/github/push \
-H "Content-Type: application/json" \
-d '{
"ref": "refs/heads/main",
"compare": "https://github.com/org/repo/compare/abc123...def456",
"pusher": {"name": "octocat"}
}'

Simulate a PR event:

curl -X POST https://your-airpipe-host/github/pull-request \
-H "Content-Type: application/json" \
-d '{"action": "opened", "number": 42}'

Simulate an issue event:

curl -X POST https://your-airpipe-host/github/issues \
-H "Content-Type: application/json" \
-d '{"action": "opened", "number": 7}'

You should see a Discord message appear in your channel within seconds.


Optional: log events to Postgres

If you want a durable audit log of every GitHub event, add the database block and LogEvent action to each interface in config.yml:

1. Run schema.sql to create the table.

2. Add to config.yml:

global:
databases:
main:
driver: postgres
conn_string: "a|ap_var::DATABASE_URL|"

3. Add a LogEvent action before NotifyDiscord in each interface:

- name: LogEvent
run_when_succeeded: [ParsePush]
database: main
query: |
INSERT INTO github_events (event_type, payload)
VALUES ('push', $1::jsonb)
RETURNING id;
params:
- a|ParsePush|

Customisation

Change to Slack

Swap DISCORD_WEBHOOK_URL for your Slack incoming webhook URL and update the body format:

body: |
{
"text": "🚀 New push to `a|ParsePush::ref|` — a|ParsePush::compare|"
}

Slack incoming webhooks accept {"text": "..."} directly.

Add more events

GitHub sends many event types (star, fork, release, workflow_run, etc.). Add a new interface per event type following the same pattern. Each interface only needs:

  1. input: a|body| to capture the payload
  2. An assertion on a field that must be present
  3. The NotifyDiscord HTTP action

Richer Discord embeds

Discord embeds support author, fields, footer, and thumbnail. See the Discord embed reference for all available properties.


Notes

  • Each interface handles one event type. GitHub lets you create multiple webhooks per repo, so you can point each event type at its own endpoint.
  • Request headers are available via a|headers| and a|headers::header-name|. If you want a single unified endpoint instead of per-event routes, read a|headers::x-github-event| to get the event type (push, pull_request, issues, etc.) and branch your logic from there.
  • Signature verification uses is_valid_hmac with a|raw_body| — the exact bytes GitHub signed, before any JSON parsing. This is a constant-time comparison so it is safe against timing attacks.
  • GitHub sends a ping event to every newly registered webhook. Each endpoint handles this gracefully — CheckEventType detects the non-matching event and returns 200, so GitHub marks the webhook as active without triggering push/PR/issue logic.

Configuration

config.yml

name: GitHubDiscord

docs: true

# Required environment variables:
# DISCORD_WEBHOOK_URL — your Discord channel webhook URL
# GITHUB_WEBHOOK_SECRET — the secret set in GitHub → Settings → Webhooks
#
# Optional (uncomment LogEvent actions and add database block):
# DATABASE_URL — Postgres connection string for event logging

interfaces:

# POST /github/push
# Receives GitHub "push" events and posts a summary to Discord.
# Configure in GitHub: Settings → Webhooks → select "Push" events.
github/push:
output: http
method: POST

actions:
- name: VerifySignature
input: a|headers|
hide_data_on_success: true
assert:
http_code_on_error: 401
error_message: "Invalid webhook signature"
tests:
- value: x-hub-signature-256
is_not_null: true
is_valid_hmac:
secret: a|ap_var::GITHUB_WEBHOOK_SECRET|
body: a|raw_body|
algorithm: sha256
prefix: "sha256="

# GitHub sends a "ping" event when the webhook is first registered.
# response_on_error short-circuits the chain and returns 200 for non-push events.
- name: CheckEventType
input: a|headers|
run_when_succeeded: [VerifySignature]
hide_data_on_success: true
response_on_error:
http_code: 200
assert:
tests:
- value: x-github-event
is_equal_to: push

- name: ParsePush
input: a|body|
run_when_succeeded: [CheckEventType]
assert:
tests:
- value: ref
is_not_null: true

- name: NotifyDiscord
run_when_succeeded: [ParsePush]
http:
url: a|ap_var::DISCORD_WEBHOOK_URL|
method: POST
headers:
content-type: application/json
body: |
{
"embeds": [
{
"title": "🚀 New push",
"description": "Branch: `a|ParsePush::ref->json_escape|`",
"url": "a|ParsePush::compare->json_escape|",
"color": 3066993
}
]
}

# POST /github/pull-request
# Receives GitHub "pull_request" events (opened, closed, merged, etc.)
# Configure in GitHub: Settings → Webhooks → select "Pull requests" events.
github/pull-request:
output: http
method: POST

actions:
- name: VerifySignature
input: a|headers|
hide_data_on_success: true
assert:
http_code_on_error: 401
error_message: "Invalid webhook signature"
tests:
- value: x-hub-signature-256
is_not_null: true
is_valid_hmac:
secret: a|ap_var::GITHUB_WEBHOOK_SECRET|
body: a|raw_body|
algorithm: sha256
prefix: "sha256="

- name: CheckEventType
input: a|headers|
run_when_succeeded: [VerifySignature]
hide_data_on_success: true
response_on_error:
http_code: 200
assert:
tests:
- value: x-github-event
is_equal_to: pull_request

- name: ParsePR
input: a|body|
run_when_succeeded: [CheckEventType]
assert:
tests:
- value: pull_request.number
is_not_null: true

- name: NotifyDiscord
run_when_succeeded: [ParsePR]
http:
url: a|ap_var::DISCORD_WEBHOOK_URL|
method: POST
headers:
content-type: application/json
body: |
{
"embeds": [
{
"title": "🔀 PR #a|ParsePR::pull_request.number->json_escape| a|ParsePR::action->json_escape|",
"description": "a|ParsePR::pull_request.title->json_escape|",
"url": "a|ParsePR::pull_request.html_url->json_escape|",
"color": 15844367
}
]
}

# POST /github/issues
# Receives GitHub "issues" events (opened, closed, labeled, etc.)
# Configure in GitHub: Settings → Webhooks → select "Issues" events.
github/issues:
output: http
method: POST

actions:
- name: VerifySignature
input: a|headers|
hide_data_on_success: true
assert:
http_code_on_error: 401
error_message: "Invalid webhook signature"
tests:
- value: x-hub-signature-256
is_not_null: true
is_valid_hmac:
secret: a|ap_var::GITHUB_WEBHOOK_SECRET|
body: a|raw_body|
algorithm: sha256
prefix: "sha256="

- name: CheckEventType
input: a|headers|
run_when_succeeded: [VerifySignature]
hide_data_on_success: true
response_on_error:
http_code: 200
assert:
tests:
- value: x-github-event
is_equal_to: issues

- name: ParseIssue
input: a|body|
run_when_succeeded: [CheckEventType]
assert:
tests:
- value: issue.number
is_not_null: true

- name: NotifyDiscord
run_when_succeeded: [ParseIssue]
http:
url: a|ap_var::DISCORD_WEBHOOK_URL|
method: POST
headers:
content-type: application/json
body: |
{
"embeds": [
{
"title": "🐛 Issue #a|ParseIssue::issue.number->json_escape| a|ParseIssue::action->json_escape|",
"description": "a|ParseIssue::issue.title->json_escape|",
"url": "a|ParseIssue::issue.html_url->json_escape|",
"color": 15158332
}
]
}

# POST /github/debug
# Echoes the full request body and headers back as JSON.
# Point any GitHub webhook event type here to inspect the raw payload.
github/debug:
output: http
method: POST

actions:
- name: VerifySignature
input: a|headers|
hide_data_on_success: true
assert:
http_code_on_error: 401
error_message: "Invalid webhook signature"
tests:
- value: x-hub-signature-256
is_not_null: true
is_valid_hmac:
secret: a|ap_var::GITHUB_WEBHOOK_SECRET|
body: a|raw_body|
algorithm: sha256
prefix: "sha256="

- name: ShowHeaders
input: a|headers|
run_when_succeeded: [VerifySignature]
hide_data_on_success: true

- name: ShowBody
input: a|body|
run_when_succeeded: [VerifySignature]
hide_data_on_success: true

- name: NotifyDiscord
run_when_succeeded: [ShowBody]
http:
url: a|ap_var::DISCORD_WEBHOOK_URL|
method: POST
headers:
content-type: application/json
body: |
{
"embeds": [
{
"title": "🔍 [debug] a|ShowHeaders::x-github-event->json_escape| — a|ShowBody::action->json_escape|",
"description": "**Repo:** a|ShowBody::repository.full_name->json_escape|\n**Sender:** a|ShowBody::sender.login->json_escape|\n**Delivery:** `a|ShowHeaders::x-github-delivery->json_escape|`\n**Issue:** a|ShowBody::issue.html_url->json_escape|\n**PR:** a|ShowBody::pull_request.html_url->json_escape|\n**Ref:** a|ShowBody::ref->json_escape|\n**Comment:** a|ShowBody::comment.body->json_escape|",
"url": "a|ShowBody::repository.html_url->json_escape|",
"color": 7506394
}
]
}

schema.sql

-- GitHub → Discord optional event log schema
-- Only needed if you enable the LogEvent action in config.yml.

CREATE TABLE IF NOT EXISTS github_events (
id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_github_events_received_at
ON github_events (received_at DESC);