{
  "name": "OwnerClue local leads — HTTP polling template",
  "active": false,
  "nodes": [
    {
      "id": "manual-start",
      "name": "Manual start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -1000,
        0
      ],
      "parameters": {}
    },
    {
      "id": "configure",
      "name": "Configure",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -780,
        0
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "api-base",
              "name": "apiBase",
              "value": "https://ownerclue.com",
              "type": "string"
            },
            {
              "id": "keyword",
              "name": "keyword",
              "value": "plumber",
              "type": "string"
            },
            {
              "id": "location-code",
              "name": "locationCode",
              "value": 0,
              "type": "number"
            },
            {
              "id": "depth",
              "name": "depth",
              "value": 20,
              "type": "number"
            },
            {
              "id": "idempotency-key",
              "name": "idempotencyKey",
              "value": "={{ 'ownerclue-search-' + $execution.id }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "notes": "Set a real location code before running. Configure HTTP Header Auth on all three HTTP Request nodes: Authorization = Bearer YOUR_KEY. Never paste an API key into this JSON.",
      "notesInFlow": true
    },
    {
      "id": "validate-configuration",
      "name": "Validate configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -560,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const config = { ...$input.first().json };\nif (!Number.isInteger(config.locationCode) || config.locationCode <= 0) throw new Error('Replace locationCode=0 with a supported code from the OwnerClue locations endpoint.');\nif (!Number.isInteger(config.depth) || config.depth < 1) throw new Error('Depth must be a positive integer.');\nif (typeof config.keyword !== 'string' || !config.keyword.trim()) throw new Error('Set a keyword.');\nif (typeof config.apiBase !== 'string' || !/^https?:\\/\\//.test(config.apiBase)) throw new Error('Set apiBase to the public app origin.');\nif (!/^[a-zA-Z0-9_-]{8,100}$/.test(config.idempotencyKey)) throw new Error('Invalid idempotency key.');\nconfig.apiBase = config.apiBase.replace(/\\/+$/, '');\nreturn [{ json: config }];"
      }
    },
    {
      "id": "create-search",
      "name": "Create search",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -340,
        0
      ],
      "parameters": {
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "method": "POST",
        "url": "={{ $json.apiBase + '/api/v1/leads/searches' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $json.idempotencyKey }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ keyword: $json.keyword, locationCode: $json.locationCode, languageCode: \"en\", depth: $json.depth }) }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 30000
        }
      },
      "notes": "Select your Header Auth credential here. HTTP failures stop execution; keep the idempotency key for an unchanged POST retry.",
      "notesInFlow": true
    },
    {
      "id": "save-task",
      "name": "Save task",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -120,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const response = $input.first().json;\nif (response.code !== 0 || typeof response.data?.id !== 'string' || !response.data.id) throw new Error('Create search did not return a successful task ID; inspect its response before retrying.');\nconst config = $('Validate configuration').first().json;\nreturn [{ json: { apiBase: config.apiBase, taskId: response.data.id, idempotencyKey: config.idempotencyKey } }];"
      },
      "notes": "Preserves the original data.id. Poll this ID to retrieve an existing search; a fresh execution creates a new key.",
      "notesInFlow": true
    },
    {
      "id": "wait",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        100,
        0
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 5,
        "unit": "seconds"
      }
    },
    {
      "id": "read-status",
      "name": "Read status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        320,
        0
      ],
      "parameters": {
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "method": "GET",
        "url": "={{ $('Save task').first().json.apiBase + '/api/v1/leads/searches/' + encodeURIComponent($('Save task').first().json.taskId) }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 30000
        }
      },
      "notes": "Select the same Header Auth credential. The URL always reads Save task, including after repeat polls.",
      "notesInFlow": true
    },
    {
      "id": "validate-status",
      "name": "Validate status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        540,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const response = $input.first().json;\nconst original = $('Save task').first().json;\nif (response.code !== 0 || response.data?.id !== original.taskId || typeof response.data?.status !== 'string') throw new Error('Unexpected task status response; inspect the original task ID.');\nreturn [{ json: response }];"
      }
    },
    {
      "id": "completed",
      "name": "Completed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        760,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "condition",
              "leftValue": "={{ $json.data.status }}",
              "rightValue": "completed",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "download-csv",
      "name": "Download CSV",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1000,
        -160
      ],
      "parameters": {
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "method": "GET",
        "url": "={{ $('Save task').first().json.apiBase + '/api/v1/leads/searches/' + encodeURIComponent($('Save task').first().json.taskId) + '/export?format=full' }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          },
          "timeout": 30000
        }
      },
      "notes": "Select the same Header Auth credential. Completed full CSV is returned as binary data; no CRM sync or mailbox validation is included.",
      "notesInFlow": true
    },
    {
      "id": "needs-inspection",
      "name": "Needs inspection",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1000,
        120
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "condition",
              "leftValue": "={{ ['failed', 'paused', 'cancelled', 'stopped'].includes($json.data.status) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "stop-for-inspection",
      "name": "Stop for inspection",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1240,
        100
      ],
      "parameters": {
        "errorType": "errorMessage",
        "errorMessage": "={{ 'Search ' + $('Save task').first().json.taskId + ' has status ' + $json.data.status + ': ' + ($json.data.error || 'Inspect the original task before retrying or resuming.') }}"
      }
    }
  ],
  "connections": {
    "Manual start": {
      "main": [
        [
          {
            "node": "Configure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure": {
      "main": [
        [
          {
            "node": "Validate configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate configuration": {
      "main": [
        [
          {
            "node": "Create search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create search": {
      "main": [
        [
          {
            "node": "Save task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save task": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "Read status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read status": {
      "main": [
        [
          {
            "node": "Validate status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate status": {
      "main": [
        [
          {
            "node": "Completed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Completed": {
      "main": [
        [
          {
            "node": "Download CSV",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Needs inspection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs inspection": {
      "main": [
        [
          {
            "node": "Stop for inspection",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 1800
  },
  "pinData": {},
  "tags": []
}
