This workflow corresponds to n8n.io template #15123 — we link there as the canonical source.
This workflow follows the Gmail → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "ABpNEZh2zrFMu9yj",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "API Mock Auto-Refresh",
"tags": [],
"nodes": [
{
"id": "b07ce725-2040-4454-83e1-6e072f23a6a5",
"name": "Sticky Note - How It Works",
"type": "n8n-nodes-base.stickyNote",
"position": [
-880,
-64
],
"parameters": {
"width": 452,
"height": 1028,
"content": "## How It Works\n\n\nThis process handles the full lifecycle of an OpenAPI spec update \u2014 from GitHub push to Postman mock refresh, spec comparison, PR comment and team notification.\n\nWhen a push lands on `develop`, the **GitHub Webhook** fires \u2192 config is loaded \u2192 **Postman mock is refreshed** \u2192 old and new specs are downloaded. GitHub Actions runs `openapi-diff` and POSTs the result back to the **Webhook OpenAPI-diff** node. If differences are found, they are parsed and cleaned, then **posted as a PR comment** and **emailed to the team**.\n\n\n---\n\n## Setup Steps\n\n1. **Import** this workflow JSON into your n8n instance.\n\n2. **Open `Set: Config`** and update:\n - `postman.apiKey` \u2014 your Postman API key\n - `postman.mockId` \u2014 your Postman Mock Server ID\n - `github.repo` \u2014 your GitHub repo (format: `owner/repo`)\n - `github.token` \u2014 a GitHub Personal Access Token with `repo` scope\n - `github.prId` \u2014 the Pull Request ID to comment on\n\n3. **Configure your GitHub Webhook** to point to `/webhook/api-update` on the `develop` branch.\n\n4. **Connect your Gmail account** in the `Send Summary Email1` node.\n\n5. **Connect your GitHub credentials** in the `Comment to PR` node (Header Auth PR).\n\n6. **Set up openapi-diff** via Docker in GitHub Actions and configure it to POST results to `/webhook/api-diff-result`.\n\n7. **Activate the workflow** and test by pushing a spec change to `develop`.\n\n Never commit real API keys or tokens to source control \u2014 use n8n credentials instead."
},
"typeVersion": 1
},
{
"id": "63156d49-3bcc-475d-8809-05b9a1547a44",
"name": "Sticky Note - GitHub Webhook",
"type": "n8n-nodes-base.stickyNote",
"position": [
-320,
-64
],
"parameters": {
"width": 432,
"height": 348,
"content": "###GitHub Webhook\n**Trigger \u2014 Entry Point**\n\nListens for `POST` requests at `/webhook/api-update`.\n\nFired by GitHub when a push or PR event occurs on the `develop` branch. Payload includes repo name, branch and PR details.\n\nConfigure this URL in your GitHub repo under **Settings \u2192 Webhooks**."
},
"typeVersion": 1
},
{
"id": "38b57660-1662-44b7-8639-1a1c4e1f13c4",
"name": "Sticky Note - Set Config",
"type": "n8n-nodes-base.stickyNote",
"position": [
144,
-64
],
"parameters": {
"height": 220,
"content": "### Set: Config\n**Load All Configuration**\n\n\n\n"
},
"typeVersion": 1
},
{
"id": "2077eb11-904a-4abc-a2d2-2cf72b8e88a1",
"name": "Sticky Note - Update Postman",
"type": "n8n-nodes-base.stickyNote",
"position": [
432,
-80
],
"parameters": {
"width": 304,
"height": 348,
"content": "### Update Postman Mock\n**Refresh Mock Server**\n\nCalls the Postman API to refresh the mock server using the configured `mockId`.\n\n\n\n"
},
"typeVersion": 1
},
{
"id": "a9c82976-2b99-45c9-aefc-cd480c9fa5e9",
"name": "Sticky Note - Old Spec",
"type": "n8n-nodes-base.stickyNote",
"position": [
800,
-64
],
"parameters": {
"width": 320,
"height": 376,
"content": "### Download OLD OpenAPI Spec\n**Fetch Baseline Spec**\n\nDownloads `openapi_old.yaml` from the `develop` branch of the GitHub repo via raw URL.\n\nThis is the **previous version** of the API spec \u2014 used as the baseline for comparison by `openapi-diff`."
},
"typeVersion": 1
},
{
"id": "3c7cde90-d2a5-408e-bd7a-02536d63a352",
"name": "Sticky Note - New Spec",
"type": "n8n-nodes-base.stickyNote",
"position": [
1152,
-32
],
"parameters": {
"width": 416,
"height": 428,
"content": "### Download NEW OpenAPI Spec\n**Fetch Updated Spec**\n\nDownloads `openapi_new.yaml` from the `develop` branch of the GitHub repo via raw URL.\n\nThis is the **latest version** of the API spec \u2014 compared against the old spec to detect breaking changes.\n\nAfter this, GitHub Actions runs `openapi-diff` (Docker) and POSTs the result back."
},
"typeVersion": 1
},
{
"id": "ed807808-15e5-4c7e-81eb-cf8aee375f46",
"name": "Sticky Note - Diff Webhook",
"type": "n8n-nodes-base.stickyNote",
"position": [
-368,
352
],
"parameters": {
"width": 368,
"height": 294,
"content": "### Webhook OpenAPI-diff\n**Receive Diff Result**\n\nA second webhook at `/webhook/api-diff-result` that receives the result POSTed back by GitHub Actions after running `openapi-diff` via Docker.\n\n"
},
"typeVersion": 1
},
{
"id": "c55a54de-86f2-495a-aafb-a8669851e39c",
"name": "Sticky Note - Check Empty",
"type": "n8n-nodes-base.stickyNote",
"position": [
96,
304
],
"parameters": {
"width": 352,
"height": 322,
"content": "### Check If Data Is Not Empty\n**Guard Clause**\n\nChecks whether the `diff` field in the incoming payload is non-empty.\n\n**True** \u2192 Proceed to parse diff and notify\n**False** \u2192 Route to Stop and Error\n\n"
},
"typeVersion": 1
},
{
"id": "69e097c7-b5ff-4bdc-8f64-54f94764d3d1",
"name": "Sticky Note - Stop Error",
"type": "n8n-nodes-base.stickyNote",
"position": [
-176,
752
],
"parameters": {
"width": 528,
"height": 248,
"content": " Stop and Error\n**Halt \u2014 No Changes Detected**\n\nTerminates the execution with the message:\n> *\"No difference is available\"*\n\n\n\n"
},
"typeVersion": 1
},
{
"id": "3dcd2e01-97ec-46ae-8f6b-7f1bd4890bde",
"name": "Sticky Note - Get Fields",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
464
],
"parameters": {
"width": 320,
"height": 454,
"content": "### Get Fields\n**Parse & Clean Diff Data**\n\nExtracts and sanitizes fields from the webhook body:\n- `owner` \u2014 first part of `repo` string\n- `repo` \u2014 second part of `repo` string\n- `pr` \u2014 Pull Request ID\n- `diff` \u2014 removes `=====` and `-----` separator lines, strips extra blank lines and trims whitespace\n\nOutput is clean Markdown-ready text."
},
"typeVersion": 1
},
{
"id": "2206905e-558a-47f8-9636-306250187dd7",
"name": "Sticky Note - Comment PR",
"type": "n8n-nodes-base.stickyNote",
"position": [
832,
448
],
"parameters": {
"width": 336,
"height": 486,
"content": "### Comment to PR\n**Post Diff to GitHub PR**\n\nCalls the GitHub REST API to post the cleaned diff as a comment on the Pull Request.\n\nEndpoint:\n`POST /repos/{owner}/{repo}/issues/{pr}/comments`\n\nThe comment is formatted under the heading:\n`### OpenAPI Diff Result`\n\nUses `Header Auth PR` credential."
},
"typeVersion": 1
},
{
"id": "ccd00b87-c68f-4e21-a945-191ca4245701",
"name": "Sticky Note - Email",
"type": "n8n-nodes-base.stickyNote",
"position": [
1264,
448
],
"parameters": {
"width": 352,
"height": 454,
"content": "### Send Summary Email1\n**Notify Team via Gmail**\n\nSends a rich HTML email to the team with:\n- Repository name and owner\n- Pull Request number\n- Full diff output (in `<pre>` block)\n- Direct link to the PR on GitHub\n\n Update `sendTo` with your team's email address.\n Uses Gmail OAuth2 credential."
},
"typeVersion": 1
},
{
"id": "e4fc1822-f992-428b-acf7-73167f479e70",
"name": "Update Postman Mock",
"type": "n8n-nodes-base.httpRequest",
"position": [
544,
80
],
"parameters": {
"url": "=https://api.getpostman.com/mocks/{{ $json.postman.mockId }}",
"options": {},
"authentication": "headerAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "5a20f6f9-e559-47ed-80ff-b1167b51a255",
"name": "Comment to PR",
"type": "n8n-nodes-base.httpRequest",
"position": [
944,
784
],
"parameters": {
"url": "=https://api.github.com/repos/{{ $json.owner }}/{{ $json.repo }}/issues/{{ $json.pr }}/comments",
"options": {},
"requestMethod": "POST",
"authentication": "headerAuth",
"jsonParameters": true,
"bodyParametersJson": "={{ JSON.stringify({\n body: \"### OpenAPI Diff Result\\n\\n\" + $json.diff\n}) }}",
"headerParametersJson": "={\n \"Accept\": \"application/vnd.github+json\",\n \"Content-Type\": \"application/json\"\n}"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "405025f2-718c-408e-b289-c12197b4adc9",
"name": "Set: Config",
"type": "n8n-nodes-base.set",
"position": [
208,
16
],
"parameters": {
"values": {
"string": [
{
"name": "postman.apiKey",
"value": "PMAK-69ae6adcaa02a000013daf43-01b4c0b7b6809dac30e2f3434f68b839fb"
},
{
"name": "postman.mockId",
"value": "26cc4d4b-9b09-4bdf-86e2-2c4c411197fe"
},
{
"name": "github.repo",
"value": "https://github.com/learning-n8n/n8n-test.git"
},
{
"name": "github.token",
"value": "github_patYOUR_AIRTABLE_TOKEN_HERE"
},
{
"name": "github.prId",
"value": "1"
}
]
},
"options": {}
},
"typeVersion": 1
},
{
"id": "357d92eb-a68d-4f84-9a48-08a4cf33cbbf",
"name": "GitHub Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-160,
144
],
"parameters": {
"path": "api-update",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"id": "30d765be-440b-4df3-bafc-d22c0990fae9",
"name": "Download OLD OpenAPI Spec",
"type": "n8n-nodes-base.httpRequest",
"position": [
912,
144
],
"parameters": {
"url": "=https://raw.githubusercontent.com/learning-n8n/n8n-test/develop/openapi_specs/openapi_old.yaml",
"options": {}
},
"typeVersion": 4.4
},
{
"id": "ce0c80c5-15a6-4959-a6b4-4501159ab10d",
"name": "Download NEW OpenAPI Spec",
"type": "n8n-nodes-base.httpRequest",
"position": [
1312,
208
],
"parameters": {
"url": "=https://raw.githubusercontent.com/learning-n8n/n8n-test/develop/openapi_specs/openapi_new.yaml\n",
"options": {}
},
"typeVersion": 4.4
},
{
"id": "a3a8fddd-5fca-4204-9c5c-0b0657f72a47",
"name": "Webhook OpenAPI-diff",
"type": "n8n-nodes-base.webhook",
"position": [
-240,
496
],
"parameters": {
"path": "api-diff-result",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "784fd60f-69cf-463a-9c6e-1a3665d3b5ee",
"name": "Stop and Error",
"type": "n8n-nodes-base.stopAndError",
"position": [
32,
864
],
"parameters": {
"errorMessage": "No difference is available"
},
"typeVersion": 1
},
{
"id": "bfd686d0-7c19-424c-a708-000d59a15508",
"name": "Get Fields",
"type": "n8n-nodes-base.set",
"position": [
592,
768
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "de0bfceb-e44e-418a-acd1-ef9fc11e0aba",
"name": "=owner",
"type": "string",
"value": "={{ $json.body.repo.split(\"/\")[0] }}"
},
{
"id": "b6f253c9-b75b-42ac-8b74-d2982f4eaef5",
"name": "repo",
"type": "string",
"value": "={{ $json.body.repo.split(\"/\")[1] }}"
},
{
"id": "c0a887ad-4ab9-48f4-b3f4-ab730f0ca903",
"name": "pr",
"type": "string",
"value": "={{ $json.body.pr }}"
},
{
"id": "a70fcc61-36e6-4b2c-88a8-c7775f546ea7",
"name": "diff",
"type": "string",
"value": "={{\n$json.body.diff\n.replace(/^[=\\-]{5,}.*$/gm,'') // remove ===== and ---- lines\n.replace(/\\n{2,}/g,'\\n') // remove extra blank lines\n.trim()\n}}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "f61c3a4d-4303-4184-a1f0-f613289b317f",
"name": "Check If data is not empty",
"type": "n8n-nodes-base.if",
"position": [
224,
480
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f70e48ee-d113-408a-a8b8-221487403489",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{$json.body.diff}}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "ceebd209-58f9-478b-9d4d-5b21d01e14d1",
"name": "Send Summary Email1",
"type": "n8n-nodes-base.gmail",
"position": [
1376,
736
],
"parameters": {
"sendTo": "user@example.com",
"message": "=<h2>OpenAPI Diff Result</h2>\n\n<p><b>Repository:</b> {{ $('Get Fields').item.json.owner }}/{{ $('Get Fields').item.json.repo }} </p>\n<p><b>Pull Request:</b> #{{ $('Get Fields').item.json.pr }} </p>\n\n<pre>{{ $('Get Fields').item.json.diff }} </pre>\n\n<p>PR Link:</p>\n<a href=\"https://github.com/{{ $('Get Fields').item.json.owner }}/{{ $('Get Fields').item.json.repo }}/pull/{{ $('Get Fields').item.json.pr }}\">\nView Pull Request\n</a>",
"options": {},
"subject": "=OpenAPI Diff Result for PR #{{ $('Get Fields').item.json.pr }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "6d9a8172-b05a-456f-90e1-661f0e41ff24",
"connections": {
"Get Fields": {
"main": [
[
{
"node": "Comment to PR",
"type": "main",
"index": 0
}
]
]
},
"Set: Config": {
"main": [
[
{
"node": "Update Postman Mock",
"type": "main",
"index": 0
}
]
]
},
"Comment to PR": {
"main": [
[
{
"node": "Send Summary Email1",
"type": "main",
"index": 0
}
]
]
},
"GitHub Webhook": {
"main": [
[
{
"node": "Set: Config",
"type": "main",
"index": 0
}
]
]
},
"Update Postman Mock": {
"main": [
[
{
"node": "Download OLD OpenAPI Spec",
"type": "main",
"index": 0
}
]
]
},
"Webhook OpenAPI-diff": {
"main": [
[
{
"node": "Check If data is not empty",
"type": "main",
"index": 0
}
]
]
},
"Download OLD OpenAPI Spec": {
"main": [
[
{
"node": "Download NEW OpenAPI Spec",
"type": "main",
"index": 0
}
]
]
},
"Check If data is not empty": {
"main": [
[
{
"node": "Get Fields",
"type": "main",
"index": 0
}
],
[
{
"node": "Stop and Error",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2httpHeaderAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This n8n workflow automates the full API specification update lifecycle whenever changes are pushed to GitHub. It refreshes a Postman mock server, downloads the old and new OpenAPI specs, compares them using , then posts detected API changes directly to the GitHub Pull Request…
Source: https://n8n.io/workflows/15123/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Automate WhatsApp communication for recruitment agencies with an interactive, structured customer experience. This workflow handles pricing inquiries, request submissions, tracking, complaints, and hu
This template turns Podium's conversation inbox into a full sales CRM with a custom funnel, AI message classification, automated drip follow-ups, daily admin reports, and a live Kanban dashboard. Six
Suspicious_login_detection. Uses postgres, httpRequest, noOp, html. Webhook trigger; 43 nodes.
This n8n workflow is designed for security monitoring and incident response when suspicious login events are detected. It can be initiated either manually from within the n8n UI for testing or automat
This workflow automates a document approval process using Supabase and Gmail. Teams that need structured multi-level document approvals. Companies managing policies, contracts, or proposals. Medical d