Skip to main content

Webhook Logger / Inspector

Category: Utilities · 📦 1 install

Get this pack →

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

FilePurpose
config.ymlAirPipe config — all endpoints
schema.sqlPostgres table + index

Endpoints

MethodPathDescription
POST/webhooks/logAccept and store any JSON payload
GET/webhooks/eventsList 100 most recent events
GET/webhooks/events/countCount all stored events
POST/webhooks/replayReplay a stored event to a target URL
POST/webhooks/events/clearDelete 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:

NameValue
DATABASE_URLpostgresql://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-Event
  • payload->'params' — URL query parameters
  • payload->'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), and a|request| (all three combined). This pack uses a|request| so nothing is lost.
  • The replay endpoint re-sends only payload.body to the target URL so the target receives the original bytes, not the stored envelope.
  • Clear stored events between test sessions with POST /webhooks/events/clear to 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);