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 Integrations → Webhooks → New 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);