{
  "name": "Appointment reminders with one-tap confirmation",
  "nodes": [
    {
      "parameters": {
        "content": "What you get: a WhatsApp reminder 24 hours before each Google Calendar appointment; the customer replies 1 to confirm or 2 to reschedule, and the calendar event is updated for you.\nWhat it replaces: reminder calls, no-shows, and reminders that keep going to people who already confirmed.\nSetup time: about 15 minutes (needs a Google Calendar credential).\n\nFewer no-shows, and you know by the morning who is coming.\n\nWorks with a normal WhatsApp number -- no Meta Business account needed.\n\nConvention: put the customer's phone number in the calendar event's description, on its own line, as `phone: +40712345678` (any format with a leading + and digits works -- non-digit characters are ignored when matching). Events without a `phone:` line are skipped.\n\nTwo entry points live in this one workflow. Branch A (left, Schedule Trigger) runs hourly, looks at events starting in roughly the next 24-48 hours, and reminds each one once -- it marks the event's description with `[reminded]` after sending, so re-running the hourly check never double-sends. Branch B (right) reacts to the customer's reply: \"1\" looks up their nearest upcoming event by phone number and marks it `[confirmed]`; \"2\" tells the customer you'll be in touch and pings the owner phone in Settings so a human can actually reschedule it (this template does not move the event itself).\n\nThe Google Calendar node parameters here are intentionally minimal (calendar, timeMin/timeMax or eventId, and a plain-text description update) to stay valid across small version differences -- if your n8n version renders the node differently, re-point \"calendar\" at your calendar and keep the description update.\n\nSetup:\n1. Create an API key at https://developers.wasync.app/keys (scopes: whatsapp.send (add whatsapp.events too if you'll use the optional Register Webhook node)).\n2. In n8n, go to Credentials -> New -> Header Auth. Name the credential \"WASync API key\". Set its Name field to `Authorization` and its Value field to `Bearer wsk_live_<your key>`.\n3. Add a Google Calendar credential in n8n (Credentials -> New -> Google Calendar account) named exactly \"Google Calendar account\" -- both Google Calendar nodes reference it by that name.\n4. Open both \"Settings\" nodes (one per branch) and fill in your own calendar ID, WASync connection id, and owner phone number. Never commit real portal URLs, tokens, connection ids, or phone numbers.\n\nPoint WASync at this workflow's Webhook node: copy its Production URL (Webhook node -> double-click -> \"Production URL\"), then either paste it at developers.wasync.app (Webhooks section) for event `message.received`, or call `PUT https://developers.wasync.app/api/v1/webhook` with `{\"url\": \"<that URL>\"}` using your API key. The disabled \"Register WASync Webhook (one-time)\" HTTP Request node below does the same call -- fill in the URL, enable it, execute it once, then disable it again.\n\nThis template uses only built-in n8n nodes, so it works on n8n Cloud; if you self-host, you can also use our community node n8n-nodes-wasync.\n\nDocs: https://docs.wasync.app/docs/guides/n8n?utm_source=n8n&utm_medium=template&utm_campaign=t8-http",
        "height": 780,
        "width": 620
      },
      "name": "Read Me First",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -320
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      },
      "name": "Hourly",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.4,
      "position": [
        0,
        500
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "Settings (Reminders)-0",
              "name": "calendarId",
              "value": "primary",
              "type": "string"
            },
            {
              "id": "Settings (Reminders)-1",
              "name": "wasyncConnectionId",
              "value": "REPLACE_WITH_YOUR_WASYNC_CONNECTION_ID",
              "type": "string"
            },
            {
              "id": "Settings (Reminders)-2",
              "name": "ownerPhone",
              "value": "REPLACE_WITH_YOUR_OWNER_WHATSAPP_NUMBER",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Settings (Reminders)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        500
      ]
    },
    {
      "parameters": {
        "resource": "event",
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Settings (Reminders)').item.json.calendarId }}"
        },
        "returnAll": true,
        "timeMin": "={{ $now.plus({ hours: 24 }).toISO() }}",
        "timeMax": "={{ $now.plus({ hours: 48 }).toISO() }}",
        "options": {}
      },
      "name": "List Upcoming Events",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        520,
        500
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "Google Calendar account"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Keep events that have a customer phone in their description (\"phone: +...\") and have not already\n// been reminded (no \"[reminded]\" marker yet).\nconst phoneLine = /phone:\\s*(\\+?[0-9][0-9 ()-]{5,})/i;\n\nconst out = [];\nfor (const item of $input.all()) {\n  const e = item.json;\n  const description = e.description || '';\n  if (description.includes('[reminded]')) continue;\n  const match = description.match(phoneLine);\n  if (!match) continue;\n  const customerPhone = match[1].replace(/[^\\d+]/g, '');\n  out.push({ json: { ...e, customerPhone } });\n}\nreturn out;"
      },
      "name": "Filter Events With Phone",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        780,
        500
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://developers.wasync.app/api/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {},
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ connectionId: $('Settings (Reminders)').item.json.wasyncConnectionId, to: $json.customerPhone, text: 'Reminder: ' + ($json.summary || 'your appointment') + ' tomorrow at ' + (($json.start && $json.start.dateTime) ? DateTime.fromISO($json.start.dateTime).toFormat('HH:mm') : '') + '. Reply 1 to confirm, 2 to reschedule.' }) }}"
      },
      "name": "Send Reminder",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1040,
        500
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "WASync API key"
        }
      }
    },
    {
      "parameters": {
        "resource": "event",
        "operation": "update",
        "eventId": "={{ $('Filter Events With Phone').item.json.id }}",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Settings (Reminders)').item.json.calendarId }}"
        },
        "updateFields": {
          "description": "={{ ($('Filter Events With Phone').item.json.description || '') + '\\n[reminded]' }}"
        }
      },
      "name": "Mark Event Reminded",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        1300,
        500
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "Google Calendar account"
        }
      }
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "wasync-inbound",
        "responseMode": "onReceived",
        "options": {}
      },
      "name": "Inbound WhatsApp Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        40
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "https://developers.wasync.app/api/v1/webhook",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {},
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ url: 'https://YOUR-N8N-INSTANCE/webhook/wasync-inbound' }) }}"
      },
      "name": "Register WASync Webhook (one-time)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        0,
        -540
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "WASync API key"
        }
      },
      "disabled": true
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "Settings (Confirmations)-0",
              "name": "calendarId",
              "value": "primary",
              "type": "string"
            },
            {
              "id": "Settings (Confirmations)-1",
              "name": "wasyncConnectionId",
              "value": "REPLACE_WITH_YOUR_WASYNC_CONNECTION_ID",
              "type": "string"
            },
            {
              "id": "Settings (Confirmations)-2",
              "name": "ownerPhone",
              "value": "REPLACE_WITH_YOUR_OWNER_WHATSAPP_NUMBER",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Settings (Confirmations)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        40
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "Reply Is 1?-cond0",
              "leftValue": "={{ $('Inbound WhatsApp Webhook').item.json.body.message.text.trim() }}",
              "rightValue": "1",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Reply Is 1 (Confirm)?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        520,
        40
      ]
    },
    {
      "parameters": {
        "resource": "event",
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Settings (Confirmations)').item.json.calendarId }}"
        },
        "returnAll": true,
        "timeMin": "={{ $now.toISO() }}",
        "timeMax": "={{ $now.plus({ days: 14 }).toISO() }}",
        "options": {}
      },
      "name": "Find Upcoming Events",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        780,
        -140
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "Google Calendar account"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Find the upcoming event whose description carries this sender's phone number, so we know which\n// appointment they're confirming.\nfunction digitsOf(s) { return String(s || '').replace(/\\D/g, ''); }\n\nconst senderPhone = digitsOf($('Inbound WhatsApp Webhook').item.json.body.message.from);\nconst phoneLine = /phone:\\s*(\\+?[0-9][0-9 ()-]{5,})/i;\n\nlet match = null;\nfor (const item of $input.all()) {\n  const e = item.json;\n  const description = e.description || '';\n  const m = description.match(phoneLine);\n  if (!m) continue;\n  if (digitsOf(m[1]) === senderPhone) { match = e; break; }\n}\n\nif (!match) return [{ json: { eventId: '', description: '', summary: '' } }];\nreturn [{ json: { eventId: match.id, description: match.description || '', summary: match.summary || '' } }];"
      },
      "name": "Match Event By Phone",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        -140
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "Event Found?-cond0",
              "leftValue": "={{ $json.eventId ? $json.eventId.length : 0 }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Event Found?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1300,
        -140
      ]
    },
    {
      "parameters": {
        "resource": "event",
        "operation": "update",
        "eventId": "={{ $json.eventId }}",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Settings (Confirmations)').item.json.calendarId }}"
        },
        "updateFields": {
          "description": "={{ ($json.description || '') + '\\n[confirmed]' }}"
        }
      },
      "name": "Mark Confirmed",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        1560,
        -220
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "Google Calendar account"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://developers.wasync.app/api/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {},
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ connectionId: $('Settings (Confirmations)').item.json.wasyncConnectionId, to: $('Inbound WhatsApp Webhook').item.json.body.message.from, text: 'Thanks for confirming! See you then.' }) }}"
      },
      "name": "Send Thank You",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1820,
        -140
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "WASync API key"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "Reply Is 2?-cond0",
              "leftValue": "={{ $('Inbound WhatsApp Webhook').item.json.body.message.text.trim() }}",
              "rightValue": "2",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Reply Is 2 (Reschedule)?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        780,
        220
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://developers.wasync.app/api/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {},
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ connectionId: $('Settings (Confirmations)').item.json.wasyncConnectionId, to: $('Inbound WhatsApp Webhook').item.json.body.message.from, text: \"We'll contact you to reschedule.\" }) }}"
      },
      "name": "Send Reschedule Notice",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1040,
        220
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "WASync API key"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://developers.wasync.app/api/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {},
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ connectionId: $('Settings (Confirmations)').item.json.wasyncConnectionId, to: $('Settings (Confirmations)').item.json.ownerPhone, text: 'Reschedule requested by ' + $('Inbound WhatsApp Webhook').item.json.body.message.from + ' on WhatsApp.' }) }}"
      },
      "name": "Notify Owner",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1300,
        220
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "WASync API key"
        }
      }
    }
  ],
  "connections": {
    "Hourly": {
      "main": [
        [
          {
            "node": "Settings (Reminders)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings (Reminders)": {
      "main": [
        [
          {
            "node": "List Upcoming Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Upcoming Events": {
      "main": [
        [
          {
            "node": "Filter Events With Phone",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Events With Phone": {
      "main": [
        [
          {
            "node": "Send Reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reminder": {
      "main": [
        [
          {
            "node": "Mark Event Reminded",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inbound WhatsApp Webhook": {
      "main": [
        [
          {
            "node": "Settings (Confirmations)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings (Confirmations)": {
      "main": [
        [
          {
            "node": "Reply Is 1 (Confirm)?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reply Is 1 (Confirm)?": {
      "main": [
        [
          {
            "node": "Find Upcoming Events",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reply Is 2 (Reschedule)?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Upcoming Events": {
      "main": [
        [
          {
            "node": "Match Event By Phone",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match Event By Phone": {
      "main": [
        [
          {
            "node": "Event Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Event Found?": {
      "main": [
        [
          {
            "node": "Mark Confirmed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Thank You",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Confirmed": {
      "main": [
        [
          {
            "node": "Send Thank You",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reply Is 2 (Reschedule)?": {
      "main": [
        [
          {
            "node": "Send Reschedule Notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reschedule Notice": {
      "main": [
        [
          {
            "node": "Notify Owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
