GitHub Webhooks → Discord
Category: Engineering & DevOps · 📦 1 install
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
| File | Purpose |
|---|---|
config.yml | AirPipe config — push, PR, and issues endpoints |
schema.sql | Optional Postgres table for event logging |
Endpoints
| Method | Path | GitHub event to select |
|---|---|---|
POST | /github/push | Push |
POST | /github/pull-request | Pull requests |
POST | /github/issues | Issues |
Each endpoint is independent. Configure only the events you care about.
Setup
1. Get a Discord webhook URL
- Open your Discord server → right-click the channel → Edit Channel
- Go to Integrations → Webhooks → New Webhook
- 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:
| Name | Value |
|---|---|
DISCORD_WEBHOOK_URL | https://discord.com/api/webhooks/your-id/your-token |
GITHUB_WEBHOOK_SECRET | the 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
| Field | Value |
|---|---|
| Payload URL | https://your-airpipe-host/github/push |
| Content type | application/json ⚠️ must be JSON, not application/x-www-form-urlencoded |
| Which events | Select 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 expectsapplication/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:
input: a|body|to capture the payload- An assertion on a field that must be present
- The
NotifyDiscordHTTP 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|anda|headers::header-name|. If you want a single unified endpoint instead of per-event routes, reada|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_hmacwitha|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
pingevent to every newly registered webhook. Each endpoint handles this gracefully —CheckEventTypedetects 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);