Back to Blog

n8n Google Maps Lead Generation: Polling Workflow Guide

Ilyas Yıldırım
Ilyas Yıldırım
13 min read

Yes, n8n can automate Google Maps lead generation without browser clicks or a custom script. The workflow starts a BasedOnB scrape through an HTTP request, checks the job until it reaches a terminal state, retrieves the results with cursor pagination, and maps each business into Google Sheets or a CRM.

The important part is not the first POST request. It is everything around it: safe retries, bounded polling, failure branches, pagination, and duplicate protection. This guide builds that complete flow for a practical example, roofing contractors in Austin, and keeps each node easy to inspect.

If you still need to decide what belongs in a useful prospect record, start with the Google Maps lead-list guide. The workflow below assumes that the category, location, target size, and destination fields are already clear.

What you need before opening n8n

Prepare these items first:

  • An n8n instance that can make outbound HTTPS requests.
  • A BasedOnB API key with scrape read and write access. It starts with bdb_live_.
  • A Google Sheet, CRM list, or database table with a stable external key column.
  • A campaign or source-event ID that can become the idempotency key.
  • A clear query, country, state, city, and target lead count.

Create the API key in the dashboard and save it in an n8n Header Auth credential. Do not paste a live key into an Edit Fields node, workflow export, screenshot, or support message. The current endpoints and response fields are listed in the localized BasedOnB API documentation.

Know the job limits before scheduling batches. One job can contain at most 10 queries. An account can have at most 2 open jobs, and the targets of all open jobs can total no more than 5,000 leads. Pending, submitted, and running jobs count as open. A queue that ignores these limits will eventually receive a 429 capacity response.

Workflow at a glance

The polling version uses this path:

  1. Schedule Trigger or Webhook receives a campaign.
  2. Edit Fields normalizes the request and sets a stable idempotency key.
  3. HTTP Request submits POST /api/v1/scrapes.
  4. IF checks statusCode; successful responses create a one-item polling state.
  5. Wait and Edit Fields increment attempt on the state branch.
  6. HTTP Request calls GET /api/v1/scrapes/:id, then Merge combines its response with that state.
  7. Switch routes HTTP errors, done, failed, cancelled, and still-open statuses.
  8. HTTP Request reads result pages after done and checks statusCode first.
  9. Each page response fans out once: body.results goes to Split Out, while one control item checks body.page.
  10. The control branch requests the next cursor once and returns that response to the same fan-out point.

The official HTTP Request node guide explains authentication, headers, query parameters, and response handling. The Wait node guide covers timed pauses and resume behaviour.

1. Build a stable request

Use an Edit Fields node after the trigger. For the Austin example, keep these values:

{
  "query": "roofing contractors",
  "country": "US",
  "state": "US.TX",
  "city": "Austin",
  "target_leads": 250
}

The request fields are intentionally plain. query defines the category or phrase. country, state, and city define the area. target_leads is the requested record count and must fit the account's available credits and open-job limits.

Add an idempotency_key field from a stable business identifier, for example roofing-austin-2026-08-batch-01. A source event ID, CRM campaign ID, or stored batch ID works well. A timestamp does not. If n8n retries with a new timestamp, the server sees a new request and can create a second job.

2. Submit the scrape

Add an HTTP Request node with these settings:

  • Method: POST
  • URL: https://www.basedonb.com/api/v1/scrapes
  • Authentication: the Header Auth credential that sends Authorization: Bearer bdb_live_...
  • Header: Idempotency-Key from the Edit Fields value
  • Body content type: JSON
  • Body: the five request fields above
  • Options > Response > Include Response Headers and Status: on
  • Options > Response > Never Error: on
  • Options > Response > Response Format: JSON

These response options make n8n return non-2xx replies as output instead of stopping the node. The full item exposes statusCode, headers, and body. Route on $json.statusCode first. Only a 2xx item may continue to business-state logic. For it, create a Poll State Edit Fields item with job_id = {{$json.body.id}}, job_status = {{$json.body.status}}, and attempt = 0. Send other status codes to the shared HTTP error branch.

A new job often returns submitted, but a reusable result can return done immediately. Inspect $json.body.status after the 2xx check, before waiting.

Reusing the same idempotency key with the same payload returns the original job. Reusing it with a different city, query, or target is a conflict. Treat the key and payload as one stored pair.

3. Poll with Wait, IF, and an attempt cap

Use the Poll State item created after the successful POST. Route its job_status:

  • done: go straight to result retrieval.
  • failed or cancelled: go to the terminal error branch.
  • pending, submitted, or running: continue to Wait.

Set Wait to a fixed interval, such as 15 seconds. After Wait, use Edit Fields to keep job_id and set attempt = {{$json.attempt + 1}}. Connect that one state item to two branches. Input 1 of a Merge node receives it directly. The other branch calls:

GET https://www.basedonb.com/api/v1/scrapes/{{$json.job_id}}

Pass the same Header Auth credential and use the same three Response options as the submit node. Send the status HTTP response to Input 2 of Merge. Set Merge to Mode: Combine and Combine By: Position. The merged item now retains job_id and attempt beside statusCode and body, so the GET response cannot erase the counter.

After Merge, check $json.statusCode before reading the body. Send non-2xx items to the shared HTTP error branch. For a 2xx response, a Switch reads $json.body.status and routes done, failed, cancelled, or an open state. A separate IF on the open branch checks $json.attempt before returning to Wait.

Add a separate IF check before returning to Wait. If attempt reaches your chosen cap, stop polling and notify an operator with the job ID. For example, 40 attempts at a 15-second interval bounds this workflow run, but it does not claim that every job finishes inside ten minutes. Do not cancel automatically at the cap. The server job may still complete, and a later check or webhook can recover it.

The failed and cancelled branch must not loop. Record the terminal status, request ID if available, and campaign reference. A new submission should be a deliberate decision, not an automatic reaction that can repeat a bad input.

4. Fetch every result page

Only request results after the job status is done:

GET https://www.basedonb.com/api/v1/scrapes/{{$json.id}}/results

Set the query parameter limit to 500. Turn on Include Response Headers and Status and Never Error, and select JSON as the Response Format, exactly as on the other HTTP Request nodes. Check $json.statusCode first and send non-2xx items to the shared error branch. A successful full response looks like this:

{
  "statusCode": 200,
  "body": {
    "id": "scrape-job-id",
    "status": "done",
    "results": [],
    "page": {
      "limit": 500,
      "next_cursor": "next-page-token",
      "has_more": true,
      "total": 1250
    }
  }
}

Keep the successful page response as one item and connect it to two branches. The data branch uses Split Out on body.results, then maps and upserts rows. The control branch does not pass through Split Out. Its IF reads $json.body.page.has_more, so it runs once per page rather than once per business.

On the true control branch, use Edit Fields to set job_id = {{$json.body.id}} and cursor = {{$json.body.page.next_cursor}}. A Next Results HTTP Request calls the same endpoint with limit=500 and that cursor, using the same Response options. Connect its response back to the same statusCode check and two-branch fan-out used by the first page. The false branch ends pagination. The public page maximum is 500, so a 1,250-row result needs three requests, not 501 or more calls caused by item fan-out.

Do not assume that total rows are present in the first response. Do not use a page number invented by the workflow. Follow the returned cursor exactly. This also makes the flow resilient if the internal storage order changes.

5. Map rows into Google Sheets or a CRM

On the data branch, use Split Out on body.results, then an Edit Fields node to keep the destination schema explicit. A useful base mapping is:

BasedOnB fieldDestination field
place_idexternal_id
titlecompany_name
categorycategory
addressstreet_address
phonephone
websitewebsite
ratinggoogle_rating
reviews_countgoogle_review_count

Use place_id as the duplicate-control key. In a CRM, choose upsert rather than blind create. In Google Sheets, look up the key and update or append as appropriate. A plain append works for a disposable test, but it will duplicate rows when an n8n execution is replayed.

Use upserts and persist the job ID plus the cursor for each page. The single-item control branch can fetch the next page while the data branch writes rows. If strict write-before-fetch ordering matters, gate the control item with one page-complete signal from the destination branch. Never connect the next-page IF after Split Out, because that would call the next cursor once per result.

The lead-list quality scorecard is a useful next step for completeness, duplicates, freshness, and source tracking. If the final list will feed an AI assistant, the AI lead-generation guide explains which work belongs to the model and which work must stay with the data source.

Common errors and safe responses

Because Never Error is on, every HTTP node must route on $json.statusCode before reading business fields. On non-2xx responses, inspect $json.body.error.code and $json.body.error.details; the body is still available for an intentional error branch.

401 Unauthorized: the key is missing, malformed, or revoked. Check the n8n credential. Never print the key in execution logs.

403 Forbidden: the key exists but lacks the required scrape scope. Create or select a minimum-scope credential that can read and write scrapes.

402 Payment Required: the account needs credits, subscription, or payment attention. Stop the branch and send it to the account owner.

409 Conflict: the same idempotency key was used with another payload. Compare the stored key and payload. Retry with the same key only after restoring the original payload; use a new stable key only for a deliberately new job.

429 Too Many Requests: inspect $json.body.error.details.reason. too_many_open_scrapes means wait for one of the 2 open jobs to finish. open_leads_limit means wait or reduce the new target so open jobs total no more than 5,000 leads. If reason is absent, treat it as request-rate limiting and use the Retry-After response header or $json.body.error.details.retry_after_seconds in a bounded Wait branch. Do not start a fast loop.

Empty results page: check job status. The results endpoint can be empty while a job is still active. Fetching results is not a substitute for the status branch.

For broader event-based pipelines, compare this flow with the Zapier and Make automation guide. The same principles apply: stable job identity, terminal-state handling, and duplicate-safe writes.

Polling or webhook?

Polling is easy to build and inspect. It fits one-off runs, low-frequency schedules, and teams learning the API. Its cost is repeated status traffic and an n8n execution that must keep track of attempts.

A webhook is better when jobs run often or completion times vary. n8n receives a completion event, verifies the webhook, and starts the result-pagination branch only then. You still need error handling, pagination, and destination upserts. A webhook changes how completion is detected; it does not change the result contract.

Start with bounded polling if it helps your team understand the state machine. Move to a webhook when repeated checks become operational noise.

FAQ

Can n8n collect Google Maps leads automatically?

Yes. n8n can call the BasedOnB REST API to start a Google Maps business search, wait while the job runs, fetch every result page, and send the rows to Google Sheets or a CRM. Keep the API key in an n8n credential rather than inside a workflow field.

Which n8n nodes does this workflow need?

Use a trigger, Edit Fields, HTTP Request, Wait, IF or Switch, Merge, Split Out, and the destination node. Merge preserves the polling counter beside the status response. Split Out is only for the data branch, so the pagination control branch remains one item.

Why should I send an Idempotency-Key?

It makes a retried POST safe. Reusing the same key with the same request returns the original job instead of creating another one. Store a stable campaign or source-event key, and do not generate it from the current time.

How often should n8n poll a scrape job?

Use a Wait node between status checks and set a maximum attempt count. A fixed 10 to 20 second interval is a practical starting point, but it is not a completion promise. Stop the loop at the cap and alert an operator instead of polling forever.

How do I retrieve more than 500 results?

Request up to 500 rows, read body.page.has_more and body.page.next_cursor from the full HTTP response, then call the results endpoint again with that cursor. Keep pagination on a single-item control branch and continue until has_more is false.

Should I use polling or a webhook in n8n?

Polling is simpler for a first workflow and for occasional jobs. A signed webhook is better for frequent or long-running automation because n8n can sleep until completion instead of making repeated status calls. Both flows still need paginated result retrieval.

What should happen when a job fails or is cancelled?

Treat failed and cancelled as terminal states. Do not request results or send the branch back to Wait. Save the job ID and status, notify the owner, and decide separately whether a new submission is appropriate.

Run a small, recoverable first job

Begin with one category, one city, and a destination table you can inspect. BasedOnB gives a new account 50 one-time export credits with no card required. Use them to verify your field mapping, idempotency key, failure branch, and upsert rule before increasing the target.

Create the credential, submit the first bounded workflow, and keep the job ID beside every imported row. That small piece of provenance makes the automation much easier to audit and repair.