Webhook Logger / Inspector
Category: Utilities · 📦 1 install
This page is generated from the Air Pipe marketplace. Browse it live to install into your organization.
A zero-dependency webhook capture tool. Point any webhook sender at /webhooks/log and the full request — body, headers, and query params — is stored in your Postgres database. Replay stored events to your local dev server without re-triggering the original source.
Useful for debugging Stripe, GitHub, Shopify, or any other webhook-based integration. Captures headers like X-Stripe-Signature, X-GitHub-Event, and X-Hub-Signature-256 alongside the body so you have everything needed to replay or verify the original request.
What's included
| File | Purpose |
|---|---|
config.yml | AirPipe config — all endpoints |
schema.sql | Postgres table + index |
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /webhooks/log | Accept and store any JSON payload |
GET | /webhooks/events | List 100 most recent events |
GET | /webhooks/events/count | Count all stored events |
POST | /webhooks/replay | Replay a stored event to a target URL |
POST | /webhooks/events/clear | Delete all stored events |
Setup
1. Create the database table
-- Run schema.sql against your database
psql $DATABASE_URL -f schema.sql
2. Set the managed variable
In the AirPipe dashboard, add a managed variable:
| Name | Value |
|---|---|
DATABASE_URL | postgresql://user:pass@host:5432/dbname |
3. Deploy the config
Point AirPipe at config.yml. In hosted mode, upload via the dashboard or CLI.
Testing
Store a webhook:
curl -X POST https://your-airpipe-host/webhooks/log \
-H "Content-Type: application/json" \
-d '{"event": "payment.completed", "amount": 4999, "currency": "usd"}'
List stored events:
curl https://your-airpipe-host/webhooks/events
Replay event ID 1 to a local server:
curl -X POST https://your-airpipe-host/webhooks/replay \
-H "Content-Type: application/json" \
-d '{"event_id": 1, "target_url": "https://your-dev-server.ngrok.io/webhook"}'
Point a real Stripe webhook here:
In Stripe Dashboard → Developers → Webhooks → Add endpoint, enter:
https://your-airpipe-host/webhooks/log
Select any events. Every Stripe event will be logged and inspectable.
Customisation
Add a source label
To track which service sent each event, add a source column to the table and a second interface per sender:
ALTER TABLE webhook_events ADD COLUMN source TEXT;
# In config.yml, add a route per sender:
webhooks/log/stripe:
output: http
method: POST
actions:
- name: CaptureRequest
input: a|request|
- name: StoreEvent
run_when_succeeded: [CaptureRequest]
database: main
query: |
INSERT INTO webhook_events (payload, source)
VALUES ($1::jsonb, 'stripe')
RETURNING id, received_at;
params:
- a|CaptureRequest|
You can also read individual parts of the captured request later in SQL:
payload->>'method'— HTTP method (POST,GET, …)payload->>'path'— request path (e.g./webhooks/log/stripe)payload->'headers'— all request headers, e.g.X-Stripe-Signature,X-GitHub-Eventpayload->'params'— URL query parameterspayload->'body'— original webhook body
Then configure your Stripe webhook to point at /webhooks/log/stripe and your GitHub webhook at /webhooks/log/github, etc.
Filter events by date
# Modify the ListEvents query:
SELECT id, payload, received_at
FROM webhook_events
WHERE received_at > NOW() - INTERVAL '1 hour'
ORDER BY received_at DESC;
Notes
- Each event is stored as
{ "method": "POST", "path": "/webhooks/log", "headers": {...}, "params": {...}, "body": {...} }— no schema enforcement on the body itself. - AirPipe exposes three request interpolation sources:
a|body|(body only),a|headers|(headers only),a|params|(query params only), anda|request|(all three combined). This pack usesa|request|so nothing is lost. - The
replayendpoint re-sends onlypayload.bodyto the target URL so the target receives the original bytes, not the stored envelope. - Clear stored events between test sessions with
POST /webhooks/events/clearto keep the table lean.
Configuration
config.yml
name: WebhookLogger
description: Capture, inspect, and replay webhooks from any source. Stores the full request — body, headers, and query params — in Postgres.
docs: true
global:
databases:
main:
driver: postgres
conn_string: "a|ap_var::DATABASE_URL|"
interfaces:
# POST /webhooks/log
# Accept any webhook and persist the full request — body, headers, and query
# params — as a single JSONB object. Point any webhook sender here.
#
# Stored shape: { "method": "POST", "path": "/webhooks/log", "headers": {...}, "params": {...}, "body": {...} }
webhooks/log:
output: http
method: POST
summary: Log webhook
description: Accept any JSON webhook and store the full request envelope — body, headers, and query params — as a single JSONB row.
tags: [webhooks]
request_example:
event: payment.completed
amount: 4999
currency: usd
customer_id: cus_abc123
actions:
- name: CaptureRequest
input: a|request|
- name: StoreEvent
run_when_succeeded: [CaptureRequest]
database: main
query: |
INSERT INTO webhook_events (payload)
VALUES ($1::jsonb)
RETURNING id, received_at;
params:
- a|CaptureRequest|
# GET /webhooks/events
# Return the 100 most recent logged events.
webhooks/events:
output: http
summary: List events
description: Return the 100 most recent logged webhook events, newest first.
tags: [webhooks]
response_example:
- id: 1
payload:
method: POST
path: /webhooks/log
headers:
content-type: application/json
x-github-event: push
params: {}
body:
event: push
repository: my-repo
received_at: "2024-01-01T12:00:00Z"
actions:
- name: ListEvents
database: main
query: |
SELECT id, payload, received_at
FROM webhook_events
ORDER BY received_at DESC
LIMIT 100;
# GET /webhooks/events/count
# Quick count of all stored events.
webhooks/events/count:
output: http
summary: Count events
description: Return the total number of stored webhook events.
tags: [webhooks]
response_example:
total: 42
actions:
- name: CountEvents
database: main
query: |
SELECT COUNT(*) AS total FROM webhook_events;
post_transforms:
- extract_value: "[0]"
# POST /webhooks/replay
# Fetch a stored event by ID and forward its payload to a target URL.
# Body: { "event_id": 42, "target_url": "https://your-dev-server/webhook" }
webhooks/replay:
output: http
method: POST
summary: Replay event
description: Fetch a stored event by ID and re-POST its original body to any target URL. Useful for replaying to a local dev server without re-triggering the source.
tags: [webhooks]
request_example:
event_id: 1
target_url: https://your-dev-server.ngrok.io/webhook
actions:
- name: ReadRequest
input: a|body|
assert:
tests:
- value: event_id
is_not_null: true
- value: target_url
is_not_null: true
- name: FetchEvent
run_when_succeeded: [ReadRequest]
database: main
query: |
SELECT id, payload, received_at
FROM webhook_events
WHERE id = $1;
params:
- a|ReadRequest::event_id|
assert:
http_code_on_error: 404
tests:
- value: count()
is_equal_to: 1
error_message: "Event not found"
post_transforms:
- extract_value: "[0]"
- name: ReplayEvent
# Replays only the original body (payload.body) so the target receives the
# same bytes the webhook sender originally sent — not the wrapped envelope.
run_when_succeeded: [FetchEvent]
http:
url: a|ReadRequest::target_url|
method: POST
headers:
content-type: application/json
body: a|FetchEvent::payload.body|
# POST /webhooks/events/clear
# Delete all stored events. Useful when cleaning up a test session.
webhooks/events/clear:
output: http
method: POST
summary: Clear events
description: Delete all stored webhook events. Use between test sessions to keep the table lean.
tags: [webhooks]
actions:
- name: ClearEvents
database: main
query: |
DELETE FROM webhook_events RETURNING id;
schema.sql
-- Webhook Logger schema
-- Run this once against your database before deploying the config.
CREATE TABLE IF NOT EXISTS webhook_events (
id BIGSERIAL PRIMARY KEY,
payload JSONB NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_webhook_events_received_at
ON webhook_events (received_at DESC);