{
  "name": "Audit recent HubSpot contacts and enrich phone contact options with QuietPass",
  "active": false,
  "nodes": [
    {
      "id": "run-audit-now",
      "name": "Run audit now",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -300,
        0
      ],
      "parameters": {}
    },
    {
      "id": "hourly-audit",
      "name": "Hourly audit",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -300,
        180
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      }
    },
    {
      "id": "configuration",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        0,
        0
      ],
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\"portalId\":\"REPLACE_HUBSPOT_PORTAL_ID\",\"defaultCountry\":\"\",\"lookbackHours\":24,\"windowEnd\":\"\",\"maximumContacts\":25,\"skipExisting\":true,\"allowPaid\":false,\"maximumMicros\":0,\"checks\":[\"basic_line_type\"]}",
        "options": {}
      }
    },
    {
      "id": "validate-audit-settings",
      "name": "Validate audit settings",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        300,
        0
      ],
      "parameters": {
        "jsCode": "function auditConfiguration(c,now=Date.now()){\n if(!/^[1-9][0-9]{0,25}$/.test(c.portalId)||!Number.isInteger(c.lookbackHours)||c.lookbackHours<1||c.lookbackHours>168||!Number.isInteger(c.maximumContacts)||c.maximumContacts<1||c.maximumContacts>100||typeof c.skipExisting!=='boolean')throw Error('Set your HubSpot portal ID, a 1–168 hour window, and a 1–100 contact batch limit.');\n const windowEnd=c.windowEnd??'';\n if(typeof windowEnd!=='string'||(windowEnd&&!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z$/.test(windowEnd)))throw Error('Leave windowEnd empty for a rolling audit, or use an ISO UTC timestamp ending in Z for a manual catch-up.');\n const end=windowEnd?Date.parse(windowEnd):now;\n if(!Number.isFinite(end)||end>now)throw Error('The audit window must end at a valid time in the past.');\n spendingGate(c,{available:true,enabled:true,currency:'USD',maximumMicros:1000000});\n return {...c,since:String(end-c.lookbackHours*3600000),until:String(end)};\n}\nfunction spendingGate(config,limits){\n if(typeof config.allowPaid!=='boolean'||!Number.isSafeInteger(config.maximumMicros)||config.maximumMicros<0||config.maximumMicros>1000000||!Array.isArray(config.checks)||!config.checks.length||config.checks.some(c=>!['basic_line_type','enhanced_line_type','current_carrier','mnp','live_status','sms','whatsapp','telegram'].includes(c)))throw Error('Review the workflow checks and spending ceiling.');\n const paid=config.checks.some(c=>c!=='basic_line_type');\n const reason=limits.available!==true?'workflow_access_unavailable':limits.enabled!==true?'purchases_paused':!['GBP','USD','EUR'].includes(limits.currency)?'currency_unavailable':paid&&config.allowPaid!==true?'paid_use_disabled':paid&&config.maximumMicros===0?'zero_spending_ceiling':config.maximumMicros>limits.maximumMicros?'workspace_ceiling_exceeded':null;\n return {allowed:reason===null,reason,currency:limits.currency,maximumMicros:config.maximumMicros};\n}\nreturn [{json:auditConfiguration($json)}];"
      }
    },
    {
      "id": "read-hubspot-account",
      "name": "Read HubSpot account",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        600,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "https://api.hubapi.com/account-info/v3/details",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "verify-hubspot-account",
      "name": "Verify HubSpot account",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        0
      ],
      "parameters": {
        "jsCode": "if(String($json.portalId)!==$('Configuration').first().json.portalId)throw Error('The HubSpot credential belongs to another portal.');return [{json:$json}];"
      }
    },
    {
      "id": "find-recently-changed-contacts",
      "name": "Find recently changed contacts",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1200,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/contacts/search",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({filterGroups:[{filters:[{propertyName:\"lastmodifieddate\",operator:\"GTE\",value:$(\"Validate audit settings\").first().json.since},{propertyName:\"lastmodifieddate\",operator:\"LTE\",value:$(\"Validate audit settings\").first().json.until},{propertyName:\"phone\",operator:\"HAS_PROPERTY\"}]}],sorts:[\"lastmodifieddate\"],properties:[\"phone\"],limit:$(\"Configuration\").first().json.maximumContacts}) }}",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "validate-and-select-batch",
      "name": "Validate and select batch",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1500,
        0
      ],
      "parameters": {
        "jsCode": "function auditContacts(page,c){\n if(!Array.isArray(page?.results)||!Number.isInteger(page.total)||page.total<0||page.total>c.maximumContacts||page.results.length!==page.total||page.paging?.next)throw Error('Recent contacts exceed the batch limit or the search is incomplete. No lookups started. Narrow lookbackHours or raise maximumContacts (up to 100); use windowEnd for bounded manual catch-up windows.');\n const ids=page.results.map(r=>String(r.id));\n if(ids.some(id=>!/^[1-9][0-9]{0,25}$/.test(id))||new Set(ids).size!==ids.length)throw Error('HubSpot returned invalid or duplicate contact IDs.');\n return ids.map(contactId=>({contactId}));\n}\nconst rows=auditContacts($json,$('Configuration').first().json);return rows.length?rows.map(json=>({json,pairedItem:{item:0}})):[{json:{state:'empty',empty:true}}];"
      }
    },
    {
      "id": "any-recent-contacts",
      "name": "Any recent contacts",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1800,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Any recent contacts",
              "leftValue": "={{ $json.empty !== true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "loop-over-contacts",
      "name": "Loop over contacts",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        2100,
        0
      ],
      "parameters": {
        "batchSize": 1,
        "options": {}
      }
    },
    {
      "id": "contact-context",
      "name": "Contact context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        600
      ],
      "parameters": {
        "jsCode": "return [{json:{contactId:$json.contactId}}];"
      }
    },
    {
      "id": "read-source-contact",
      "name": "Read source contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        300,
        600
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.hubapi.com/crm/v3/objects/contacts/\" + $(\"Contact context\").item.json.contactId + \"?properties=phone,hs_country_region_code,qp_phone_request_id,qp_phone_checked_at,qp_phone_e164,qp_phone_status&propertiesWithHistory=phone\" }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "source-identity",
      "name": "Source identity",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        600
      ],
      "parameters": {
        "jsCode": "function phoneRevision(contact){\n const history=contact.propertiesWithHistory?.phone;\n if(!Array.isArray(history)||!history.length)return null;\n const sorted=history.filter(v=>typeof v.timestamp==='string'&&Number.isFinite(Date.parse(v.timestamp))).sort((a,b)=>Date.parse(b.timestamp)-Date.parse(a.timestamp));\n return sorted[0]?.timestamp??null;\n}\nfunction contactIdentity(contact,config){\n if(!/^[1-9][0-9]{0,25}$/.test(config.contactId)||!/^[1-9][0-9]{0,25}$/.test(config.portalId)||String(contact.id)!==config.contactId||contact.archived===true)throw Error('Check the contact ID and HubSpot credential account.');\n const revision=phoneRevision(contact),phone=contact.properties?.phone;\n return {contactId:config.contactId,revision,phone:typeof phone==='string'?phone:'',country:contact.properties?.hs_country_region_code||config.defaultCountry||'',needsReview:!revision||!phone,reason:!phone?'empty_phone':!revision?'phone_history_required':null};\n}\nreturn [{json:contactIdentity($json,{...$('Configuration').first().json,contactId:$('Contact context').item.json.contactId})}];"
      }
    },
    {
      "id": "has-phone-history",
      "name": "Has phone history",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        900,
        600
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Has phone history",
              "leftValue": "={{ $json.needsReview === false }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "format-source-phone",
      "name": "Format source phone",
      "type": "n8n-nodes-quietpass.quietPass",
      "typeVersion": 1,
      "position": [
        1200,
        600
      ],
      "parameters": {
        "resource": "phoneNumber",
        "operation": "format",
        "phone": "={{ $json.phone }}",
        "defaultCountry": "={{ $json.country }}",
        "options": {}
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "valid-source-phone",
      "name": "Valid source phone",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1500,
        600
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Valid source phone",
              "leftValue": "={{ $json.number?.valid === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "read-workspace-limits",
      "name": "Read workspace limits",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        0,
        1200
      ],
      "parameters": {
        "method": "GET",
        "url": "https://quietpass.com/v1/automation/settings",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "quietPassApi",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "spending-permission",
      "name": "Spending permission",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        300,
        1200
      ],
      "parameters": {
        "jsCode": "function spendingGate(config,limits){\n if(typeof config.allowPaid!=='boolean'||!Number.isSafeInteger(config.maximumMicros)||config.maximumMicros<0||config.maximumMicros>1000000||!Array.isArray(config.checks)||!config.checks.length||config.checks.some(c=>!['basic_line_type','enhanced_line_type','current_carrier','mnp','live_status','sms','whatsapp','telegram'].includes(c)))throw Error('Review the workflow checks and spending ceiling.');\n const paid=config.checks.some(c=>c!=='basic_line_type');\n const reason=limits.available!==true?'workflow_access_unavailable':limits.enabled!==true?'purchases_paused':!['GBP','USD','EUR'].includes(limits.currency)?'currency_unavailable':paid&&config.allowPaid!==true?'paid_use_disabled':paid&&config.maximumMicros===0?'zero_spending_ceiling':config.maximumMicros>limits.maximumMicros?'workspace_ceiling_exceeded':null;\n return {allowed:reason===null,reason,currency:limits.currency,maximumMicros:config.maximumMicros};\n}\nreturn [{json:spendingGate($('Configuration').first().json,$json)}];"
      }
    },
    {
      "id": "purchases-allowed",
      "name": "Purchases allowed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        600,
        1200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Purchases allowed",
              "leftValue": "={{ $json.allowed === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "prepare-saved-operation",
      "name": "Prepare saved operation",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        900,
        1200
      ],
      "parameters": {
        "method": "POST",
        "url": "https://quietpass.com/v1/automation/operations",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "quietPassApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ workflowId:\"n8n:hubspot:\"+$(\"Configuration\").first().json.portalId+\":audit-v1:\"+$(\"Configuration\").first().json.checks.map(c=>[\"basic_line_type\",\"enhanced_line_type\",\"current_carrier\",\"mnp\",\"live_status\",\"sms\",\"whatsapp\",\"telegram\"].indexOf(c)).sort().join(\"-\"), recordId:$(\"Source identity\").item.json.contactId, revision:$(\"Source identity\").item.json.revision, phone:$(\"Format source phone\").item.json.number.e164, checks:$(\"Configuration\").first().json.checks, currency:$(\"Spending permission\").item.json.currency, maximumMicros:$(\"Configuration\").first().json.maximumMicros }) }}",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "run-original-operation",
      "name": "Run original operation",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1200,
        1200
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ \"https://quietpass.com/v1/automation/operations/\" + $json.id + \"/run\" }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "quietPassApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({allowPaid:$(\"Configuration\").first().json.allowPaid}) }}",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "read-retained-result",
      "name": "Read retained result",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1500,
        1200
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://quietpass.com/v1/automation/operations/\" + $(\"Prepare saved operation\").item.json.id }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "quietPassApi",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "result-is-settled",
      "name": "Result is settled",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1800,
        1200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Result is settled",
              "leftValue": "={{ $json.state === \"settled\" && $json.resultState === \"settled\" && !!$json.result?.requestId }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "read-current-contact",
      "name": "Read current contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        0,
        1800
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.hubapi.com/crm/v3/objects/contacts/\" + $(\"Contact context\").item.json.contactId + \"?properties=phone,hs_country_region_code,qp_phone_request_id&propertiesWithHistory=phone\" }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "format-current-phone",
      "name": "Format current phone",
      "type": "n8n-nodes-quietpass.quietPass",
      "typeVersion": 1,
      "position": [
        300,
        1800
      ],
      "parameters": {
        "resource": "phoneNumber",
        "operation": "format",
        "phone": "={{ $json.properties.phone || \"\" }}",
        "defaultCountry": "={{ $json.properties.hs_country_region_code || $(\"Configuration\").first().json.defaultCountry }}",
        "options": {}
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "guard-current-source",
      "name": "Guard current source",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        1800
      ],
      "parameters": {
        "jsCode": "function phoneRevision(contact){\n const history=contact.propertiesWithHistory?.phone;\n if(!Array.isArray(history)||!history.length)return null;\n const sorted=history.filter(v=>typeof v.timestamp==='string'&&Number.isFinite(Date.parse(v.timestamp))).sort((a,b)=>Date.parse(b.timestamp)-Date.parse(a.timestamp));\n return sorted[0]?.timestamp??null;\n}\nfunction retainedResult(operation){\n const r=operation?.result;\n return operation?.state==='settled'&&operation.resultState==='settled'&&typeof r?.requestId==='string'&&r.number?.valid===true&&typeof r.number.e164==='string';\n}\nfunction writeGuard(contact,formatted,operation,identity){\n const reason=!retainedResult(operation)?'result_not_ready':String(contact.id)!==identity.contactId?'contact_changed':phoneRevision(contact)!==identity.revision?'source_revision_changed':formatted.number?.valid!==true||formatted.number.e164!==operation.result.number.e164?'source_phone_changed':contact.properties?.qp_phone_request_id===operation.result.requestId?'already_written':null;\n if(reason)return {write:false,reason,operationId:operation.id};\n const r=operation.result,s=r.contact?.sms??{},scalar=v=>typeof v==='boolean'?String(v):typeof v==='string'?v.slice(0,300):'';\n const reasons=[r.number.validationReason,r.number.typeReason,r.number.carrierReason,r.number.portedReason,s.reason,r.contact?.whatsapp?.reason,r.contact?.telegram?.reason,r.network?.reason].filter(v=>typeof v==='string');\n const fields={e164:r.number.e164,country:r.number.country,valid:r.number.valid,type:r.number.type,carrier:r.number.carrier,ported:r.number.ported,sms:s.supported,sms_basis:s.basis,whatsapp:r.contact?.whatsapp?.registered,telegram:r.contact?.telegram?.registered,reachable:r.network?.reachable,status:reasons.length?'partial':'completed',reason:[...new Set(reasons)].join('; '),checked_at:r.preparedAt,request_id:r.requestId};\n return {write:true,operationId:operation.id,requestId:r.requestId,contactId:identity.contactId,revision:identity.revision,properties:Object.fromEntries(Object.entries(fields).map(([k,v])=>['qp_phone_'+k,scalar(v)]))};\n}\nreturn [{json:writeGuard($('Read current contact').item.json,$json,$('Read retained result').item.json,$('Source identity').item.json)}];"
      }
    },
    {
      "id": "safe-to-write",
      "name": "Safe to write",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        900,
        1800
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Safe to write",
              "leftValue": "={{ $json.write === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "write-quietpass-fields",
      "name": "Write QuietPass fields",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1200,
        1800
      ],
      "parameters": {
        "method": "PATCH",
        "url": "={{ \"https://api.hubapi.com/crm/v3/objects/contacts/\" + $json.contactId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({properties:$json.properties}) }}",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "write-receipt",
      "name": "Write receipt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1500,
        1800
      ],
      "parameters": {
        "jsCode": "const r=$('Guard current source').item.json,p=r.properties;return [{json:{state:'written',contactId:r.contactId,revision:r.revision,operationId:r.operationId,requestId:r.requestId,contactOptions:['whatsapp','telegram'].filter(k=>p['qp_phone_'+k]==='true'),smsSupported:p.qp_phone_sms,smsBasis:p.qp_phone_sms_basis,reviewReason:p.qp_phone_reason}}];"
      }
    },
    {
      "id": "review-without-purchase",
      "name": "Review without purchase",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2600,
        600
      ],
      "parameters": {
        "jsCode": "return [{json:{state:'review',reason:$json.reason||$json.error?.message||'invalid_or_ambiguous_phone',contactId:$('Contact context').item.json.contactId,sourceRevision:$('Source identity').item.json.revision}}];"
      }
    },
    {
      "id": "retain-operation-for-review",
      "name": "Retain operation for review",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2600,
        1200
      ],
      "parameters": {
        "jsCode": "return [{json:{state:'review',reason:$json.reason||'result_pending_or_expired',operationId:$('Prepare saved operation').item.json.id,contactId:$('Source identity').item.json.contactId,sourceRevision:$('Source identity').item.json.revision}}];"
      }
    },
    {
      "id": "check-prior-receipt",
      "name": "Check prior receipt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1800,
        600
      ],
      "parameters": {
        "jsCode": "function phoneRevision(contact){\n const history=contact.propertiesWithHistory?.phone;\n if(!Array.isArray(history)||!history.length)return null;\n const sorted=history.filter(v=>typeof v.timestamp==='string'&&Number.isFinite(Date.parse(v.timestamp))).sort((a,b)=>Date.parse(b.timestamp)-Date.parse(a.timestamp));\n return sorted[0]?.timestamp??null;\n}\nfunction needsAudit(contact,formatted,config){\n const revision=phoneRevision(contact),p=contact.properties??{},checked=Date.parse(p.qp_phone_checked_at);\n return !(config.skipExisting&&revision&&Number.isFinite(checked)&&checked>=Date.parse(revision)&&p.qp_phone_request_id&&['completed','partial'].includes(p.qp_phone_status)&&p.qp_phone_e164===formatted.number?.e164);\n}\nreturn [{json:{needed:needsAudit($('Read source contact').item.json,$('Format source phone').item.json,$('Configuration').first().json)}}];"
      }
    },
    {
      "id": "needs-a-fresh-check",
      "name": "Needs a fresh check",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2100,
        600
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Needs a fresh check",
              "leftValue": "={{ $json.needed === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "keep-existing-result",
      "name": "Keep existing result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2600,
        1800
      ],
      "parameters": {
        "jsCode": "return [{json:{state:'skipped',reason:'unchanged_phone_already_checked',contactId:$('Contact context').item.json.contactId}}];"
      }
    },
    {
      "id": "audit-summary",
      "name": "Audit summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2500,
        0
      ],
      "parameters": {
        "jsCode": "const rows=$input.all().map(i=>i.json).filter(r=>!r.empty);return [{json:{contacts:rows.length,updated:rows.filter(r=>r.state==='written').length,skipped:rows.filter(r=>r.state==='skipped').length,review:rows.filter(r=>r.state==='review').length,reviewRecords:rows.filter(r=>r.state==='review'),contactOptions:rows.filter(r=>r.state==='written').map(r=>({contactId:r.contactId,apps:r.contactOptions,smsSupported:r.smsSupported,smsBasis:r.smsBasis,reviewReason:r.reviewReason})),note:'App registration and number status do not establish consent, ownership, or delivery. No messages sent.'}}];"
      }
    },
    {
      "id": "start-here-crm-phone-hygiene",
      "name": "Start here — CRM phone hygiene",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1100,
        0
      ],
      "parameters": {
        "content": "## Audit recently changed CRM contacts before outreach\nFor CRM and marketing-operations teams: find recently changed HubSpot contacts, reject bad phone data before buying checks, populate phone/network/app fields, and report records needing attention. No prospect messages are sent.\n\n1. Add the verified QuietPass node (LIVE on n8n Cloud; npm 0.1.2 for self-hosted).\n2. Create the 15 qp_phone_* text fields listed in the guide. Select your HubSpot Service Key on every HubSpot HTTP node and QuietPass API credential on its HTTP and formatter nodes. Never paste keys into Configuration.\n3. Enter your portal ID. Start with a short lookbackHours and maximumContacts: 25 (hard maximum 100). More matches stop before any lookups; this is a bounded audit, not an unlimited backfill. For manual catch-up set windowEnd to a past ISO UTC timestamp ending in Z; clear it before enabling the hourly schedule.\n4. Free first run: basic_line_type only, allowPaid false, maximumMicros 0. In QuietPass enable saved workflows at zero limits. Run Audit now and inspect Audit summary plus the CRM fields.\n5. After reviewing results, publish/activate this workflow to use its hourly schedule. Imports start inactive. Overlapping windows skip unchanged phone receipts; failed HTTP calls stop the run for review.\n\nFull setup and recovery: https://quietpass.com/integrations/n8n/",
        "width": 1000,
        "height": 820,
        "color": 7
      }
    },
    {
      "id": "optional-paid-checks-and-interpretation",
      "name": "Optional paid checks and interpretation",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1100,
        900
      ],
      "parameters": {
        "content": "## Add network and messaging checks when you choose\nIn Configuration select checks from basic_line_type, enhanced_line_type, current_carrier, mnp, live_status, sms, whatsapp, telegram. Set allowPaid true and a reviewed maximumMicros; 50000 = 0.05 of your wallet currency. Authorize per-lookup and daily spending in QuietPass first. Limits are shared with other saved workflows.\n\nWhen changing check selection, set skipExisting false for the reviewed run; unchanged CRM receipts do not record which checks were purchased. The operation identity includes the selected check set and real phone-history timestamp. Replays reuse that original operation, not a newly purchased lookup.\n\nWhatsApp/Telegram true only identifies registration. SMS support can be assumed from mobile line type; it is not a delivery test. Unknown clears stale fields and remains unknown, never false. Availability does not authorize contacting someone.\n\nIf a CRM write fails, retain the operation ID, contact ID, and original phone revision from that execution and use the separate recovery download within 24 hours. Recovery never purchases. Rereading before PATCH reduces but cannot eliminate concurrent-edit races. Restrict execution access and retention.",
        "width": 1000,
        "height": 820,
        "color": 7
      }
    }
  ],
  "connections": {
    "Run audit now": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hourly audit": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Validate audit settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate audit settings": {
      "main": [
        [
          {
            "node": "Read HubSpot account",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read HubSpot account": {
      "main": [
        [
          {
            "node": "Verify HubSpot account",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify HubSpot account": {
      "main": [
        [
          {
            "node": "Find recently changed contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find recently changed contacts": {
      "main": [
        [
          {
            "node": "Validate and select batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate and select batch": {
      "main": [
        [
          {
            "node": "Any recent contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any recent contacts": {
      "main": [
        [
          {
            "node": "Loop over contacts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Audit summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop over contacts": {
      "main": [
        [
          {
            "node": "Audit summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Contact context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Contact context": {
      "main": [
        [
          {
            "node": "Read source contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read source contact": {
      "main": [
        [
          {
            "node": "Source identity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Source identity": {
      "main": [
        [
          {
            "node": "Has phone history",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has phone history": {
      "main": [
        [
          {
            "node": "Format source phone",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Review without purchase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format source phone": {
      "main": [
        [
          {
            "node": "Valid source phone",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valid source phone": {
      "main": [
        [
          {
            "node": "Check prior receipt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Review without purchase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check prior receipt": {
      "main": [
        [
          {
            "node": "Needs a fresh check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs a fresh check": {
      "main": [
        [
          {
            "node": "Read workspace limits",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Keep existing result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read workspace limits": {
      "main": [
        [
          {
            "node": "Spending permission",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Spending permission": {
      "main": [
        [
          {
            "node": "Purchases allowed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Purchases allowed": {
      "main": [
        [
          {
            "node": "Prepare saved operation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Review without purchase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare saved operation": {
      "main": [
        [
          {
            "node": "Run original operation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run original operation": {
      "main": [
        [
          {
            "node": "Read retained result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read retained result": {
      "main": [
        [
          {
            "node": "Result is settled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Result is settled": {
      "main": [
        [
          {
            "node": "Read current contact",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Retain operation for review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read current contact": {
      "main": [
        [
          {
            "node": "Format current phone",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format current phone": {
      "main": [
        [
          {
            "node": "Guard current source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Guard current source": {
      "main": [
        [
          {
            "node": "Safe to write",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Safe to write": {
      "main": [
        [
          {
            "node": "Write QuietPass fields",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Retain operation for review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write QuietPass fields": {
      "main": [
        [
          {
            "node": "Write receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write receipt": {
      "main": [
        [
          {
            "node": "Loop over contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Review without purchase": {
      "main": [
        [
          {
            "node": "Loop over contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retain operation for review": {
      "main": [
        [
          {
            "node": "Loop over contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep existing result": {
      "main": [
        [
          {
            "node": "Loop over contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "none",
    "saveManualExecutions": true,
    "executionTimeout": 600,
    "timezone": "UTC"
  },
  "pinData": {},
  "tags": []
}
