AutomationFlowsWeb Scraping › Venuedesk - Create Recurring From Calendar

Venuedesk - Create Recurring From Calendar

VenueDesk - Create Recurring From Calendar. Uses httpRequest. Webhook trigger; 23 nodes.

Webhook trigger★★★★☆ complexity23 nodesHTTP Request
Web Scraping Trigger: Webhook Nodes: 23 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "name": "VenueDesk - Create Recurring From Calendar",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "create-recurring-from-calendar",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "crc-webhook-001",
      "name": "Webhook: Create Recurring From Calendar",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -600,
        -400
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "jsCode": "// Parse incoming payload (handles chunked or direct JSON)\nconst items = $input.all();\nlet rawData = items[0]?.json || {};\nlet finalResult = {};\n\ntry {\n  if (rawData.hasOwnProperty('0')) {\n    const combinedString = Object.values(rawData).join('');\n    finalResult = JSON.parse(combinedString);\n  } else if (rawData.body && typeof rawData.body === 'object') {\n    finalResult = rawData.body;\n  } else if (typeof rawData.body === 'string') {\n    finalResult = JSON.parse(rawData.body);\n  } else {\n    finalResult = rawData;\n  }\n} catch (e) {\n  finalResult = rawData;\n  finalResult._error = 'Parsing failed: ' + e.message;\n}\n\n// Resolve monthly_fee: new 4-week cycle payloads send monthly_fee directly;\n// legacy payloads may send monthly_rate \u2014 fall back gracefully.\nconst monthlyFee = parseFloat(\n  finalResult.monthly_fee ||\n  finalResult.agreed_price ||\n  finalResult.monthly_rate ||\n  '0'\n) || 0;\n\nconst output = {\n  ...finalResult,\n  tenant_id:         parseInt(finalResult.tenant_id || 1001),\n  day_of_week:       parseInt(finalResult.day_of_week ?? -1),\n  payment_amount:    parseFloat(finalResult.payment_amount || '0') || 0,\n  rate_per_session:  parseFloat(finalResult.rate_per_session || '0') || 0,\n  monthly_fee:       monthlyFee,\n  agreed_price:      monthlyFee,\n  total_cycles:      parseInt(finalResult.total_cycles || '0') || 0,\n  remaining_cycles:  parseInt(finalResult.remaining_cycles || finalResult.total_cycles || '0') || 0,\n  billing_day:       parseInt(finalResult.billing_day || '1') || 1,\n  billing_type:      finalResult.billing_type || 'monthly',\n  interaction_type:  finalResult.interaction_type || 'RECURRING_CONTRACT_CREATED',\n  contract_notes:    finalResult.contract_notes || '',\n  status:            finalResult.status || 'confirmed',\n  booking_source:    finalResult.booking_source || 'calendar_recurring',\n  _debug_parsed:     true\n};\n\nreturn [{ json: output }];"
      },
      "id": "crc-parse-001",
      "name": "Parse: Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -380,
        -400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.venuedesk.co.uk/recurring/upsert-customer",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($('Parse: Input').first().json?.jwt || '') }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ customer_name: ($('Parse: Input').first().json?.customer_name || '').trim(), customer_email: ($('Parse: Input').first().json?.customer_email || '').trim().toLowerCase(), customer_phone: ($('Parse: Input').first().json?.customer_phone || '').trim() }) }}",
        "options": {}
      },
      "id": "crc-api-upsert-cust-001",
      "name": "API: Upsert Customer",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -160,
        -400
      ]
    },
    {
      "parameters": {
        "jsCode": "// Flatten adapter \u2014 preserves $('DB: Upsert Customer') named references in Code: Validate\nconst resp = $input.first().json;\nreturn [{ json: resp.data || resp }];"
      },
      "id": "crc-flatten-upsert-cust-001",
      "name": "DB: Upsert Customer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        60,
        -400
      ]
    },
    {
      "parameters": {
        "jsCode": "const wb = $('Parse: Input').first().json;\n\nconst specificDates = (wb.specific_dates || '').trim();\nconst ratePerSession = parseFloat(wb.rate_per_session || '0') || 0;\nconst billingType    = (wb.billing_type || 'monthly').toLowerCase();\nconst paymentTiming  = wb.payment_timing || 'in_advance';\n\nif (!specificDates) {\n  return [{ json: { error: 'Missing specific_dates', date_count: 0, dates: '', payment_timing: paymentTiming } }];\n}\n\nconst dates = specificDates.split(',').map(d => d.trim()).filter(d => /^\\d{4}-\\d{2}-\\d{2}$/.test(d)).sort();\n\nconst firstDate = dates[0] || '';\nconst lastDate  = dates[dates.length - 1] || '';\n\nlet firstPeriodEnd = '';\nif (firstDate) {\n  const d = new Date(firstDate + 'T12:00:00');\n  d.setDate(d.getDate() + 27);\n  firstPeriodEnd = d.getFullYear() + '-'\n    + String(d.getMonth() + 1).padStart(2, '0') + '-'\n    + String(d.getDate()).padStart(2, '0');\n}\n\nconst cyclePrice = parseFloat(wb.monthly_fee || wb.agreed_price || '0') || ratePerSession * 4;\n\nreturn [{ json: {\n  dates:          dates.join(','),\n  date_count:     dates.length,\n  payment_timing: paymentTiming,\n  billing_type:   billingType,\n  first_period_start: firstDate,\n  first_period_end:   firstPeriodEnd,\n  first_month_amount: cyclePrice.toFixed(2),\n  monthly_periods_csv:  firstDate,\n  monthly_ends_csv:     firstPeriodEnd,\n  monthly_amounts_csv:  cyclePrice.toFixed(2),\n  monthly_sessions_csv: String(dates.length),\n  monthly_count:        1,\n  period_start:         firstDate,\n  period_end:           firstPeriodEnd\n} }];"
      },
      "id": "crc-gen-dates-001",
      "name": "Code: Generate Dates",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        280,
        -400
      ]
    },
    {
      "parameters": {
        "jsCode": "const wb = $('Parse: Input').first().json;\nconst customerRow = $('DB: Upsert Customer').first().json;\nconst dateGen = $('Code: Generate Dates').first().json;\n\nconst customer_id = customerRow?.customer_id;\nconst room_id = wb?.room_id;\nconst tenant_id = wb?.tenant_id;\n\nif (!customer_id) {\n  throw new Error('Validation Error: No customer_id returned. Check DB: Upsert Customer.');\n}\nif (!room_id || !tenant_id) {\n  throw new Error(`Validation Error: Missing room_id or tenant_id. Room: ${room_id}, Tenant: ${tenant_id}`);\n}\nif (!dateGen.dates || dateGen.date_count === 0) {\n  throw new Error('Validation Error: No dates found in specific_dates.');\n}\n\nreturn [{\n  json: {\n    customer_id,\n    room_id,\n    tenant_id,\n    dates: dateGen.dates,\n    date_count: dateGen.date_count,\n    first_month_amount: dateGen.first_month_amount,\n    customer_name: customerRow.full_name || wb.customer_name,\n    customer_email: customerRow.email || wb.customer_email\n  }\n}];"
      },
      "id": "crc-validate-001",
      "name": "Code: Validate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        500,
        -400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.venuedesk.co.uk/recurring/check-clashes",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($('Parse: Input').first().json?.jwt || '') }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ dates: $('Code: Validate').first().json.dates || '', room_id: $('Code: Validate').first().json.room_id, start_time: $('Parse: Input').first().json?.start_time || '09:00', end_time: $('Parse: Input').first().json?.end_time || '23:59' }) }}",
        "options": {
          "continueOnFail": true
        }
      },
      "id": "crc-api-clash-001",
      "name": "API: Check Clashes",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        720,
        -400
      ]
    },
    {
      "parameters": {
        "jsCode": "// Flatten adapter \u2014 Code: Filter Dates reads $input.first().json.clashed_dates\nconst resp = $input.first().json;\nconst data = resp.data || resp;\nreturn [{ json: { clashed_dates: data.clashed_dates || [] } }];"
      },
      "id": "crc-flatten-clash-001",
      "name": "DB: Check Room Clashes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        -400
      ]
    },
    {
      "parameters": {
        "jsCode": "// Skip-on-clash filter \u2014 drops ONLY clashed dates, preserves all others\n// (including those AFTER a clash). Earlier revision used `d < firstConflict`\n// which truncated the entire series from the first conflict onwards, losing\n// legitimate non-clashed sessions later in the schedule.\nconst validate = $('Code: Validate').first().json;\nconst allDates = (validate.dates || '').split(',').filter(Boolean).sort();\nconst clashRow = $input.first().json;\nconst clashed  = new Set((clashRow.clashed_dates || []).map(d => String(d).trim().slice(0,10)));\n\nif (clashed.size === 0) {\n  return [{ json: {\n    filtered_dates: validate.dates || '',\n    dates_count:    allDates.length,\n    skipped_count:  0,\n    has_conflict:   false,\n    blocked:        false,\n    warning:        null\n  }}];\n}\n\nconst safeDates    = allDates.filter(d => !clashed.has(d));\nconst sortedClashed = [...clashed].sort();\nconst firstConflict = sortedClashed[0];\n\nconst fmtDate = d => {\n  const dt = new Date(d + 'T12:00:00');\n  return dt.toLocaleDateString('en-GB', {day:'numeric', month:'long', year:'numeric'});\n};\n\nif (safeDates.length === 0) {\n  return [{ json: {\n    filtered_dates:      '',\n    dates_count:         0,\n    skipped_count:       allDates.length,\n    has_conflict:        true,\n    blocked:             true,\n    first_conflict_date: firstConflict,\n    warning: 'All ' + allDates.length + ' requested dates are already booked for this room. Please choose different dates or a different room.'\n  }}];\n}\n\nreturn [{ json: {\n  filtered_dates:      safeDates.join(','),\n  dates_count:         safeDates.length,\n  skipped_count:       clashed.size,\n  has_conflict:        true,\n  blocked:             false,\n  first_conflict_date: firstConflict,\n  clashed_count:       clashed.size,\n  warning: 'Recurring series created for ' + safeDates.length + ' non-clashing session(s). ' + clashed.size + ' clashed date(s) skipped (first: ' + fmtDate(firstConflict) + ').'\n}}];"
      },
      "id": "crc-filter-001",
      "name": "Code: Filter Dates",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1160,
        -400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "avail-cond-001",
              "leftValue": "={{ $('Code: Filter Dates').first().json.blocked }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "crc-if-avail-001",
      "name": "IF: Room Available?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1380,
        -400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.venuedesk.co.uk/recurring/create-series-calendar",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($('Parse: Input').first().json?.jwt || '') }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ customer_id: $('Code: Validate').first().json.customer_id, room_id: $('Code: Validate').first().json.room_id, series_name: $('Parse: Input').first().json?.series_name || ($('Code: Validate').first().json.customer_name + ' Block'), cycle_amount: parseFloat($('Parse: Input').first().json?.monthly_fee || $('Parse: Input').first().json?.agreed_price || $('Parse: Input').first().json?.monthly_rate || $('Parse: Input').first().json?.rate_per_session || 0) || 0, payment_amount: parseFloat($('Parse: Input').first().json?.payment_amount || 0) || 0, start_time: $('Parse: Input').first().json?.start_time || '', end_time: $('Parse: Input').first().json?.end_time || '', frequency: $('Parse: Input').first().json?.frequency || 'weekly', end_date: $('Parse: Input').first().json?.end_date || '', day_of_week: parseInt($('Parse: Input').first().json?.day_of_week ?? 1), payment_timing: $('Parse: Input').first().json?.payment_timing || 'in_advance', billing_type: $('Parse: Input').first().json?.billing_type || $('Parse: Input').first().json?.billing_frequency || 'monthly', cycle_length_weeks: ($('Parse: Input').first().json?.payment_timing === 'in_full') ? null : (parseInt($('Parse: Input').first().json?.cycle_length_weeks || 4) || 4), notes: $('Parse: Input').first().json?.notes || '' }) }}",
        "options": {}
      },
      "id": "crc-api-insert-series-cal-001",
      "name": "API: Insert Series Calendar",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1600,
        -400
      ]
    },
    {
      "parameters": {
        "jsCode": "// Flatten adapter \u2014 named 'DB: Insert Rule' to preserve ALL downstream references:\n// DB: Insert Bookings body reads $('DB: Insert Rule').first().json.series_id / .series_name\n// Respond: Created reads $('DB: Insert Rule').first().json.cycle_amount / .balance_due / .series_reference\n// API: Log Interaction body reads $('DB: Insert Rule').first().json.series_reference\n// create-series-calendar returns: { rule_id, series_id, series_reference, series_name, cycle_amount, balance_due }\nconst resp = $input.first().json;\nreturn [{ json: resp.data || resp }];"
      },
      "id": "crc-flatten-rule-001",
      "name": "DB: Insert Rule",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1820,
        -400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.venuedesk.co.uk/recurring/insert-bookings",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($('Parse: Input').first().json?.jwt || '') }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ customer_id: $('Code: Validate').first().json.customer_id, room_id: $('Code: Validate').first().json.room_id, dates_csv: $('Code: Filter Dates').first().json.filtered_dates || '', start_time: $('Parse: Input').first().json?.start_time || '', end_time: $('Parse: Input').first().json?.end_time || '', rate_per_session: parseFloat($('Parse: Input').first().json?.rate_per_session || 0) || 0, rule_id: null, series_id: $('DB: Insert Rule').first().json.series_id || null, series_label: $('DB: Insert Rule').first().json.series_name || 'Recurring', status: $('Parse: Input').first().json?.status || 'confirmed' }) }}",
        "options": {}
      },
      "id": "crc-api-insert-bookings-001",
      "name": "API: Insert Bookings",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2040,
        -400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.venuedesk.co.uk/recurring/record-payment",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($('Parse: Input').first().json?.jwt || '') }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ customer_id: $('Code: Validate').first().json.customer_id, payment_amount: parseFloat($('Parse: Input').first().json?.payment_amount || 0) || 0, payment_type: $('Parse: Input').first().json?.payment_type || 'none', payment_method: $('Parse: Input').first().json?.payment_method || 'cash', series_reference: $('DB: Insert Rule').first().json.series_reference || '', series_id: $('DB: Insert Rule').first().json.series_id || null }) }}",
        "options": {
          "continueOnFail": true
        }
      },
      "id": "crc-api-record-payment-001",
      "name": "API: Record Initial Payment",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2260,
        -400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.venuedesk.co.uk/recurring/log-interaction",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($('Parse: Input').first().json?.jwt || '') }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ customer_id: $('Code: Validate').first().json.customer_id, series_reference: $('DB: Insert Rule').first().json.series_reference || '', room_name: $('Parse: Input').first().json?.room_name || '', frequency: $('Parse: Input').first().json?.frequency || 'weekly', date_count: $('Code: Generate Dates').first().json?.date_count || 0, staff_member: $('Parse: Input').first().json?.performed_by || 'Staff', interaction_type: $('Parse: Input').first().json?.interaction_type || 'RECURRING_CONTRACT_CREATED', notes: $('Parse: Input').first().json?.contract_notes || '' }) }}",
        "options": {
          "continueOnFail": true
        }
      },
      "id": "crc-api-log-interaction-001",
      "name": "API: Log Interaction",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2040,
        -180
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({\n  success: true,\n  status: 'created',\n  series_id: $('DB: Insert Rule').first().json.series_id,\n  series_name: $('DB: Insert Rule').first().json.series_name || null,\n  series_reference: $('DB: Insert Rule').first().json.series_reference || null,\n  customer_id: $('Code: Validate').first().json.customer_id,\n  customer_name: $('Code: Validate').first().json.customer_name,\n  booking_count: $('Code: Filter Dates').first().json.dates_count,\n  cycle_amount: $('DB: Insert Rule').first().json.cycle_amount || null,\n  balance_due_initial: $('DB: Insert Rule').first().json.balance_due || null,\n  frequency: $('Parse: Input').first().json.frequency || null,\n  booking_source: $('Parse: Input').first().json.booking_source || 'calendar_recurring',\n  partial_booking: $('Code: Filter Dates').first().json.has_conflict || false,\n  warning: $('Code: Filter Dates').first().json.warning || null,\n  first_conflict_date: $('Code: Filter Dates').first().json.first_conflict_date || null\n}) }}",
        "options": {
          "responseCode": 201,
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "crc-respond-001",
      "name": "Respond: Created",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2480,
        -400
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, status: 'unavailable', message: $('Code: Filter Dates').first().json.warning || 'Room not available for the requested dates.' }) }}",
        "options": {
          "responseCode": 409
        }
      },
      "id": "crc-respond-blocked-001",
      "name": "Respond: Not Available",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1600,
        -180
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "crfc-card-cond-001",
              "leftValue": "={{ $('Parse: Input').first().json?.payment_method || '' }}",
              "rightValue": "card",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "crfc-if-card-001",
      "name": "IF: Card?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2260,
        -560
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.venuedesk.co.uk/stripe/session",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($('Parse: Input').first().json?.jwt || '') }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify((function(){\n  const parse    = $('Parse: Input').first().json || {};\n  const rule     = $('DB: Insert Rule').first().json || {};\n  const valid    = $('Code: Validate').first().json || {};\n  const filtered = $('Code: Filter Dates').first().json || {};\n  const timing   = parse.payment_timing || 'in_full';\n  const seriesTotal = parseFloat(parse.rate_per_session || 0) * parseInt(filtered.dates_count || 0);\n  let cycle1 = null;\n  try { cycle1 = $('Code: Pick Cycle 1').first().json; } catch(e) { cycle1 = null; }\n  const isCadenced = timing !== 'in_full' && cycle1 && !cycle1._seed_failed;\n  const amount = isCadenced ? parseFloat(cycle1.cycle_amount || 0) : seriesTotal;\n  const descParts = [];\n  descParts.push('Recurring series \u2014 ' + (rule.series_name || 'Block'));\n  if (isCadenced && timing === 'in_advance') descParts.push('(Cycle ' + (cycle1.cycle_number||1) + ' / ' + cycle1.sessions_count + ' sessions)');\n  else if (isCadenced && timing === 'in_arrears') descParts.push('(Card on file \u2014 billed after each cycle)');\n  else descParts.push('(' + (filtered.dates_count || 0) + ' sessions)');\n  return {\n    amount,\n    recurring_series_id: rule.series_id,\n    cycle_id:            isCadenced ? cycle1.cycle_id : '',\n    payment_timing:      timing,\n    customer_id:         valid.customer_id,\n    description:         descParts.join(' '),\n    success_url: 'https://andyjay72.github.io/VenueDesk/CommunityHub/checkout.html?session_id={CHECKOUT_SESSION_ID}',\n    cancel_url:  'https://andyjay72.github.io/VenueDesk/CommunityHub/calendar.html'\n  };\n})()) }}",
        "options": {}
      },
      "id": "crfc-api-stripe-001",
      "name": "API: Create Stripe Session",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2480,
        -640
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({  success: true,  status: 'pending_payment',  stripe_url: $input.first().json?.url || $input.first().json?.data?.url || null,  series_id: $('DB: Insert Rule').first().json.series_id,  series_name: $('DB: Insert Rule').first().json.series_name || null,  series_reference: $('DB: Insert Rule').first().json.series_reference || null,  customer_id: $('Code: Validate').first().json.customer_id,  customer_name: $('Code: Validate').first().json.customer_name,  booking_count: $('Code: Filter Dates').first().json.dates_count,  cycle_amount: $('DB: Insert Rule').first().json.cycle_amount || null,  frequency: $('Parse: Input').first().json.frequency || null,  booking_source: $('Parse: Input').first().json.booking_source || 'calendar_recurring',  partial_booking: $('Code: Filter Dates').first().json.has_conflict || false,  skipped_count: $('Code: Filter Dates').first().json.skipped_count || 0,  warning: $('Code: Filter Dates').first().json.warning || null,  first_conflict_date: $('Code: Filter Dates').first().json.first_conflict_date || null}) }}",
        "options": {
          "responseCode": 201,
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "crfc-respond-stripe-001",
      "name": "Respond: Stripe",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2700,
        -640
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "cadenced-check",
              "leftValue": "={{ ($('Parse: Input').first().json?.payment_timing || 'in_advance').toLowerCase() }}",
              "rightValue": "in_full",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "crfc-if-cadenced-001",
      "name": "IF: Cadenced?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2400,
        200
      ],
      "notes": "Feature C \u2014 splits cadenced (in_advance/in_arrears) from in_full. TRUE materialises schedule rows; FALSE falls through to existing bulk Stripe."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.venuedesk.co.uk/recurring/seed-cycle-schedule",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + ($('Parse: Input').first().json?.jwt || '') }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ series_id: $('DB: Insert Rule').first().json.series_id, customer_id: $('Code: Validate').first().json.customer_id, dates_csv: $('Code: Filter Dates').first().json.filtered_dates, rate_per_session: parseFloat($('Parse: Input').first().json?.rate_per_session || 0), cycle_length_weeks: parseInt($('Parse: Input').first().json?.cycle_length_weeks || 4), payment_timing: $('Parse: Input').first().json?.payment_timing || 'in_advance', frequency: $('Parse: Input').first().json?.frequency || 'weekly' }) }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": false,
              "neverError": true
            }
          },
          "timeout": 20000
        }
      },
      "id": "crfc-seed-cycle-schedule-001",
      "name": "API: Seed Cycle Schedule",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "continueOnFail": true,
      "position": [
        2600,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "const seedResp = $input.first().json;\nconst cycles   = seedResp?.data?.cycles || [];\nconst ids      = seedResp?.data?.schedule_ids || [];\n\nif (cycles.length === 0 || ids.length === 0) {\n  return [{ json: { _seed_failed: true, seedResp } }];\n}\n\nconst cycle1 = cycles[0];\nconst cycle1Id = ids[0];\n\nconst parse  = $('Parse: Input').first().json || {};\nconst rule   = $('DB: Insert Rule').first().json || {};\nconst valid  = $('Code: Validate').first().json || {};\n\nreturn [{\n  json: {\n    cycle_id:            cycle1Id,\n    cycle_amount:        parseFloat(cycle1.amount || 0),\n    cycle_number:        cycle1.cycleNumber || 1,\n    period_start:        cycle1.periodStart,\n    period_end:          cycle1.periodEnd,\n    payment_timing:      parse.payment_timing || 'in_advance',\n    recurring_series_id: rule.series_id,\n    series_name:         rule.series_name || 'Block',\n    customer_id:         valid.customer_id,\n    sessions_count:      cycle1.sessions || 0,\n  }\n}];"
      },
      "id": "crfc-pick-cycle1-001",
      "name": "Code: Pick Cycle 1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2800,
        200
      ]
    }
  ],
  "connections": {
    "Webhook: Create Recurring From Calendar": {
      "main": [
        [
          {
            "node": "Parse: Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse: Input": {
      "main": [
        [
          {
            "node": "API: Upsert Customer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: Upsert Customer": {
      "main": [
        [
          {
            "node": "DB: Upsert Customer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DB: Upsert Customer": {
      "main": [
        [
          {
            "node": "Code: Generate Dates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Generate Dates": {
      "main": [
        [
          {
            "node": "Code: Validate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Validate": {
      "main": [
        [
          {
            "node": "API: Check Clashes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: Check Clashes": {
      "main": [
        [
          {
            "node": "DB: Check Room Clashes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DB: Check Room Clashes": {
      "main": [
        [
          {
            "node": "Code: Filter Dates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Filter Dates": {
      "main": [
        [
          {
            "node": "IF: Room Available?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Room Available?": {
      "main": [
        [
          {
            "node": "API: Insert Series Calendar",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond: Not Available",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: Insert Series Calendar": {
      "main": [
        [
          {
            "node": "DB: Insert Rule",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DB: Insert Rule": {
      "main": [
        [
          {
            "node": "API: Insert Bookings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: Insert Bookings": {
      "main": [
        [
          {
            "node": "API: Log Interaction",
            "type": "main",
            "index": 0
          },
          {
            "node": "IF: Cadenced?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: Record Initial Payment": {
      "main": [
        [
          {
            "node": "Respond: Created",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: Log Interaction": {
      "main": []
    },
    "IF: Card?": {
      "main": [
        [
          {
            "node": "API: Create Stripe Session",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "API: Record Initial Payment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: Create Stripe Session": {
      "main": [
        [
          {
            "node": "Respond: Stripe",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Cadenced?": {
      "main": [
        [
          {
            "node": "API: Seed Cycle Schedule",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF: Card?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: Seed Cycle Schedule": {
      "main": [
        [
          {
            "node": "Code: Pick Cycle 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Pick Cycle 1": {
      "main": [
        [
          {
            "node": "IF: Card?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "crfc-v6-field-fix-2026-05-24",
  "id": "CreateRecurringFromCalendar",
  "tags": []
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

VenueDesk - Create Recurring From Calendar. Uses httpRequest. Webhook trigger; 23 nodes.

Source: https://github.com/AndyJay72/VenueDesk/blob/main/n8n-workflows/CreateRecurringFromCalendar.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di

n8n, Execute Workflow Trigger, HTTP Request +1
Web Scraping

This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .

HTTP Request, Ssh
Web Scraping

This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia

HTTP Request
Web Scraping

This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c

HTTP Request
Web Scraping

Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.

Execute Command, HTTP Request, Read Write File +1