# Save any webhook data to a database automatically with n8n — free workflow JSON

> Source: <https://dev.to/flowkithq/save-any-webhook-data-to-a-database-automatically-with-n8n-free-workflow-json-14lh>
> Published: 2026-05-22 10:07:15+00:00

Every webhook is a data goldmine you're probably wasting.
When Stripe charges a customer, when GitHub pushes a commit, when Typeform receives a response â€” your app fires a webhook. That data arrives once, gets processed, and disappears.
Most developers handle webhooks reactively: catch the event, do the thing, move on. But every webhook is also a record â€” a timestamped, structured snapshot of what happened in your business.
Here's a 4-node n8n workflow that saves every incoming webhook to a Postgres database automatically. Set it up once, and you'll have a queryable log of every webhook event your systems generate.
A persistent HTTP endpoint that accepts POST requests. Any service that supports webhooks â€” Stripe, GitHub, Shopify, Typeform, Twilio, HubSpot, or your own app â€” can send events here. The URL stays the same forever; no polling needed.
A JavaScript Code node that pulls the fields you care about and standardizes them into a consistent shape, regardless of which service sent the webhook:
const payload = $input.first().json;\nconst body = payload.body || payload;\nreturn [{\n json: {\n source: payload.headers?.['x)-github-event'] ? 'github'\n : payload.headers?.['stripe-signature'] ? 'stripe'\n : payload.headers?.['x)webhook-source'] || 'unknown',\n event_type: body.type || body.event || body.action || 'raw',\n payload: JSON.stringify(body),\n received_at: new Date().toISOString()\n }\n}];\n```
\n\nFor Stripe you'd use `body.type` (e.g. `payment_intent.succeeded`). For GitHub, `body.action` and `body.repository.name`. For Typeform, `body.form_response.answers`. The Code node lets you handle all of them in one place.
### Node 3 â€” Postgres: INSERT
Writes the normalized record to your database. Supports Postgres, MySQL, SQLite â€” or swap for Google Sheets, Airtable, or Notion if you don't have a database yet.
### Node 4 â€” Respond to Webhook
Returns HTTP 200 immediately after the database write. Best practice: always respond within 5 seconds or the sender will retry and create duplicate records.
---
## Setup (10 minutes)
1. **Import the JSON** into n8n (New Workflow â†’ Import from clipboard)
2. **Create the table** in your database:
``` sql
CREATE TABLE webhook_log (
id SERIAL PRIMARY KEY,
source TEXT,
event_type TEXT,
payload JSONB,
received_at TIMESTAMP DEFAULT NOW()
);
https://your-n8n.com/webhook/webhook-to-db
Test it: send a test event, then run SELECT * FROM webhook_log
to confirm the row appeared.
{
"name": "Webhook to Database",
"nodes": [
{"parameters":{"httpMethod":"POST","path":"webhook-to-db","responseMode":"responseNode","options":{}},"id":"wb1","name":"Receive Webhook","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[240,300]},
{"parameters":{"jsCode":"const payload = $input.first().json;\nconst body = payload.body || payload;\nreturn [{\n json: {\n source: payload.headers?.['x-github-event'] ? 'github'\n : payload.headers?.['stripe-signature'] ? 'stripe'\n : payload.headers?.['x-webhook-source'] || 'unknown',\n event_type: body.type || body.event || body.action || 'raw',\n payload: JSON.stringify(body),\n received_at: new Date().toISOString()\n }\n}];"},"id":"wb2","name":"Extract Fields","type":"n8n-nodes-base.code","typeVersion":2,"position":[460,300]},
{"parameters":{"operation":"executeQuery","query":"INSERT INTO webhook_log (source, event_type, payload, received_at) VALUES ('{{ $json.source }}', '{{ $json.event_type }}', '{{ $json.payload }}'::jsonb, '{{ $json.received_at }}'::timestamp) RETURNING id"},"id":"wb3","name":"Save to Database","type":"n8n-nodes-base.postgres","typeVersion":2.3,"position":[680,300]},
{"parameters":{"respondWith":"json","responseBody":"={"ok": true, "id": {{ $json.id }}}"},"id":"wb4","name":"Return 200 OK","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[900,300]}
],
"connections": {
"Receive Webhook":{"main":[[{"node":"Extract Fields","type":"main","index":0}]]},
"Extract Fields":{"main":[[{"node":"Save to Database","type":"main","index":0}]]},
"Save to Database":{"main":[[{"node":"Return 200 OK","type":"main","index":0}]]}
},
"settings":{"executionOrder":"v1"},
"tags":[{"name":"data"}]
}
Use JSONB, not TEXT, for the payload column
Postgres JSONB lets you query inside the payload with ->
operators:
-- Find all Stripe payments over $50
SELECT received_at, payload->>'amount'
FROM webhook_log
WHERE source = 'stripe'
AND event_type = 'payment_intent.succeeded'
AND (payload->>'amount')::int > 5000;
This turns your webhook log into a lightweight analytics layer â€” no separate BI tool needed.
Add deduplication to handle retries
Services retry webhooks if they don't get a 200 within 5 seconds. Add a unique constraint on the event ID:
ALTER TABLE webhook_log ADD COLUMN event_id TEXT UNIQUE;
In Node 2: event_id: body.id || body.event_id || null
â€” duplicate events get rejected at the DB level, silently.
Route to different tables by source
Add an IF/Switch node between Extract Fields and Save to Database. Route Stripe events to stripe_events
, GitHub events to github_events
. Faster queries, cleaner schema.
Alert on high-value events
Add a parallel branch after Extract Fields: IF event_type === 'payment_intent.succeeded'
â†’ Slack node. You get a Slack ping every time a payment lands, while the database write still happens in the main branch.
Use Google Sheets instead of Postgres
No database? Swap the Postgres node for a Google Sheets "Append Row" node. You lose JSONB querying but gain a shareable spreadsheet log that non-technical teammates can read.
SELECT DATE(received_at), COUNT(*) FROM webhook_log WHERE event_type = 'payment_intent.succeeded' GROUP BY 1
event_type LIKE '%failed%'
or '%error%'
This workflow is simple. What you build on top of it is not.
This workflow is part of our 15-template n8n automation bundle â€” each one covering a different business use case: lead capture, invoice generation, AI customer support, social media automation, price monitoring, and more.
Grab the full bundle at stripeai.gumroad.com â€” pre-tested, documented, ready to activate.
Built with n8n. Self-hostable, open source, no vendor lock-in.
