{"slug": "save-any-webhook-data-to-a-database-automatically-with-n8n-free-workflow-json", "title": "Save any webhook data to a database automatically with n8n — free workflow JSON", "summary": "Free, four-node n8n workflow that automatically saves incoming webhook data from services like Stripe, GitHub, and Typeform into a PostgreSQL database. The workflow uses a persistent HTTP endpoint to receive POST requests, a JavaScript code node to normalize the data into a consistent format, and a database node to insert the records before returning a 200 response. The author provides the complete workflow JSON and setup instructions, claiming the process takes about 10 minutes to implement.", "body_md": "Every webhook is a data goldmine you're probably wasting.\nWhen 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.\nMost 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.\nHere'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.\nA 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.\nA JavaScript Code node that pulls the fields you care about and standardizes them into a consistent shape, regardless of which service sent the webhook:\nconst 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\\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.\n### Node 3 â€” Postgres: INSERT\nWrites 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.\n### Node 4 â€” Respond to Webhook\nReturns HTTP 200 immediately after the database write. Best practice: always respond within 5 seconds or the sender will retry and create duplicate records.\n---\n## Setup (10 minutes)\n1. **Import the JSON** into n8n (New Workflow â†’ Import from clipboard)\n2. **Create the table** in your database:\n``` sql\nCREATE TABLE webhook_log (\nid SERIAL PRIMARY KEY,\nsource TEXT,\nevent_type TEXT,\npayload JSONB,\nreceived_at TIMESTAMP DEFAULT NOW()\n);\nhttps://your-n8n.com/webhook/webhook-to-db\nTest it: send a test event, then run SELECT * FROM webhook_log\nto confirm the row appeared.\n{\n\"name\": \"Webhook to Database\",\n\"nodes\": [\n{\"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]},\n{\"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]},\n{\"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]},\n{\"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]}\n],\n\"connections\": {\n\"Receive Webhook\":{\"main\":[[{\"node\":\"Extract Fields\",\"type\":\"main\",\"index\":0}]]},\n\"Extract Fields\":{\"main\":[[{\"node\":\"Save to Database\",\"type\":\"main\",\"index\":0}]]},\n\"Save to Database\":{\"main\":[[{\"node\":\"Return 200 OK\",\"type\":\"main\",\"index\":0}]]}\n},\n\"settings\":{\"executionOrder\":\"v1\"},\n\"tags\":[{\"name\":\"data\"}]\n}\nUse JSONB, not TEXT, for the payload column\nPostgres JSONB lets you query inside the payload with ->\noperators:\n-- Find all Stripe payments over $50\nSELECT received_at, payload->>'amount'\nFROM webhook_log\nWHERE source = 'stripe'\nAND event_type = 'payment_intent.succeeded'\nAND (payload->>'amount')::int > 5000;\nThis turns your webhook log into a lightweight analytics layer â€” no separate BI tool needed.\nAdd deduplication to handle retries\nServices retry webhooks if they don't get a 200 within 5 seconds. Add a unique constraint on the event ID:\nALTER TABLE webhook_log ADD COLUMN event_id TEXT UNIQUE;\nIn Node 2: event_id: body.id || body.event_id || null\nâ€” duplicate events get rejected at the DB level, silently.\nRoute to different tables by source\nAdd an IF/Switch node between Extract Fields and Save to Database. Route Stripe events to stripe_events\n, GitHub events to github_events\n. Faster queries, cleaner schema.\nAlert on high-value events\nAdd a parallel branch after Extract Fields: IF event_type === 'payment_intent.succeeded'\nâ†’ Slack node. You get a Slack ping every time a payment lands, while the database write still happens in the main branch.\nUse Google Sheets instead of Postgres\nNo 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.\nSELECT DATE(received_at), COUNT(*) FROM webhook_log WHERE event_type = 'payment_intent.succeeded' GROUP BY 1\nevent_type LIKE '%failed%'\nor '%error%'\nThis workflow is simple. What you build on top of it is not.\nThis 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.\nGrab the full bundle at stripeai.gumroad.com â€” pre-tested, documented, ready to activate.\nBuilt with n8n. Self-hostable, open source, no vendor lock-in.", "url": "https://wpnews.pro/news/save-any-webhook-data-to-a-database-automatically-with-n8n-free-workflow-json", "canonical_source": "https://dev.to/flowkithq/save-any-webhook-data-to-a-database-automatically-with-n8n-free-workflow-json-14lh", "published_at": "2026-05-22 10:07:15+00:00", "updated_at": "2026-05-22 10:36:35.944530+00:00", "lang": "en", "topics": ["developer-tools", "data", "enterprise-software", "open-source", "products"], "entities": ["n8n", "Stripe", "GitHub", "Typeform", "Shopify", "Twilio", "HubSpot", "Postgres"], "alternates": {"html": "https://wpnews.pro/news/save-any-webhook-data-to-a-database-automatically-with-n8n-free-workflow-json", "markdown": "https://wpnews.pro/news/save-any-webhook-data-to-a-database-automatically-with-n8n-free-workflow-json.md", "text": "https://wpnews.pro/news/save-any-webhook-data-to-a-database-automatically-with-n8n-free-workflow-json.txt", "jsonld": "https://wpnews.pro/news/save-any-webhook-data-to-a-database-automatically-with-n8n-free-workflow-json.jsonld"}}