# PlaceCall - AI phone calls to US businesses (`voygr/placecall`) Actor

Calls every business on your list, asks the questions you set, and returns a structured row per call. Gets what scraping cannot: whether they actually answer, and what they actually say.

- **URL**: https://apify.com/voygr/placecall.md
- **Developed by:** [VOYGR](https://apify.com/voygr) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$100.00 / 1,000 charged calls

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Phone-verify business locations

Bring a list of businesses. This Actor calls each one, asks the questions you
set, and returns a structured row per call.

**Every other Actor here reads what a business published. This one phones and
asks.**

That matters because published data goes stale in ways scraping cannot see. A
listing says open; the number is disconnected. Hours say 9pm; the kitchen
stops at 8. The website says walk-ins welcome; they stopped in March. None of
that is on a page anywhere - it only exists on the phone.

**Try it first, free:** leave **Check my list, don't call** ticked and press
Start. The Actor reads your list and shows the columns it found, the exact
questions each business would get, the rows it would skip and the most the
calls could cost. Nobody is called and nothing is charged.

### What you put in

A list of businesses, from wherever you have one:

- **A CSV file or link.** Upload a CSV (or tab-separated) file with a header
  row, or paste a link to one. A Google Sheet link works if the sheet is
  shared as "Anyone with the link can view" or published as CSV. Excel files:
  save as CSV first.
- **Cells pasted from a spreadsheet**, header row included.
- **The dataset of a previous Actor run**, for example Google Maps results.
- **JSON**, one object per business.

If you give more than one, the first in this order is used: dataset, CSV,
pasted cells, JSON. **Columns are detected automatically**, so an export from
a scraper, a CRM or a spreadsheet works without mapping; fax and
owner-mobile columns rank below the business phone.

- **US numbers in any common format** work: `(415) 555-0142`, `415.555.0142`,
  `+1 415 555 0142`. They are converted to international format before
  dialling. Numbers outside the US are skipped with a reason.
- **The same number twice is called once.** Every row sharing it gets the
  result, the extras marked `duplicateOf`.
- **What the list needs:** a column of US phone numbers (any header; it is
  found from the values). A business-name column helps, since the agent says
  the name on the call. Every other column is passed straight through to the
  output, so results join back to your own records, and can be used in
  questions.

A sample to copy into a sheet or the paste box:

```
Business Name	Phone	City	My ID
Corner Cafe	(415) 555-0142	San Francisco	1001
Night Owl Diner	(415) 555-0199	Oakland	1002
```

### Questions

The form shows the questions themselves: what you see is what the agent
asks, of **every** business, one at a time. It starts with the opening-hours
pair; edit or replace them freely.

**Read a business its own data back** with `{column}`, any column from your
list: `Is your address still {address}?` Matching ignores case, spaces and
underscores, so `{Business Name}` works too. If your list has no such column
the run stops before calling anyone and lists the columns it does have; if
only some rows have a value, those rows get one question fewer rather than a
question with a hole in it. The run log shows the questions, filled in for a
sample business, before the first call.

Ready-made sets, tested wording:

| Checking | Questions |
|---|---|
| Opening hours | Are you open right now? / What are your hours today? |
| Reservations | Do you take reservations, or is it walk-in only? / For a party of 6, how far ahead should someone book? |
| Availability | Do you currently have oat milk? / Roughly what does it cost? |
| New customers | Are you accepting new patients right now? / What is the wait for a first appointment? |
| Details on file | Is your address still {address}? / Is this the best number for customers to reach you on? |

API callers can also pass `template` (`verify_hours`, `reservations`,
`availability`, `new_customers`, `confirm_details`) with an empty `questions`
list, and `templateVariables` for a set's placeholders, e.g.
`{"item": "oat milk"}`.

**"Still in business?" is not a question, on purpose.** If they are gone,
nobody answers, so the answer is already in `reached: false` plus the
outcome. Ask the opening-hours pair and read that column.

### What you get back

One row per business:

```json
{
  "name": "Corner Cafe",
  "phone": "+14155550142",
  "phoneRaw": "(415) 555-0142",
  "reached": true,
  "answers": [
    { "item": "Are you open right now?", "status": "answered", "answer": "Yes, open now." },
    { "item": "What are your hours today?", "status": "answered", "answer": "7 AM to 3 PM." }
  ],
  "questionsAsked": ["Are you open right now?", "What are your hours today?"],
  "keyInfo": "Open now; closes at 3 PM today.",
  "outcomeType": "success_no_booking",
  "billedCents": 10,
  "callId": "00000000-0000-4000-8000-000000000000",
  "attempts": [ { "attempt": 1, "outcomeType": "success_no_booking" } ],
  "reachability": { "signal": "answered", "evidence": "Answered on attempt 1 (1 of 1 attempts answered)." },
  "transcript": [ "..." ],
  "source": { "...your original row...": "" }
}
```

`answers` is the call analyzer's own reading, as it wrote it. It usually
matches your questions one for one, but it can split a question in two or
merge two into one, so match on `item` rather than on position. The
questions actually asked for that business are in `questionsAsked`.

**`reached: false` is a result, not an error.** For anyone verifying a list,
"nobody picks up" or "the number is disconnected" is exactly the signal worth
paying for.

**Nothing is dropped quietly.** Rows that cannot be called (no usable phone,
outside the US) come back with the reason. Businesses past your call budget
or a daily limit come back as `notAttempted` with the reason. If you abort a
run gracefully, every business it had not finished gets a row saying so; a
hard abort stops the run at once and cannot write them. `retryable` on each
row says whether a later retry pass will call it again. A check-only run
writes one row per business with `dryRun: true` and the exact
`questionsToAsk`.

### What the call history says about a business

Every attempt is kept, the failed ones included, because for anyone verifying
a list the failures are findings. Each row carries its full `attempts`
history (when each call was placed, how long it lasted, how it ended, and on
calls that did not reach the business, what the line itself said), plus a
one-line `reachability` read built from all of it. **The most recent evidence
wins**: a business that answered today is answering, whatever a voicemail said
last week.

| Signal | What it means |
|---|---|
| `answered` | Someone at the business picked up, even if they refused or did not understand. |
| `closure_language` | The business's own line said it stopped operating: "permanently closed", "out of business", "no longer in business". |
| `number_not_in_service` | The carrier said the number is dead: "not in service", "disconnected". The business may exist; this number does not reach it. |
| `moved` | The line said the business moved or changed number. Update the record, do not close it. |
| `wrong_number` | Someone answered, but it was not this business. The listed number may have been reassigned. |
| `never_answered` | No attempt reached anyone. The evidence line gives the count, how many days it spans and how each attempt ended. |
| `line_problem` | Only busy lines or technical failures so far. Says nothing about the business yet. |
| `callback_requested` | Someone asked to be called back later; `callAfter` says when. The line is live. |
| `not_called` | No call was placed (skipped, over budget, or a check-only run). |

This is evidence, not a verdict. A plain "we're closed right now" is
deliberately **not** read as closure, because that is a live business outside
its hours. `never_answered` across several days is a strong hint a listing is
stale, but we do not know the business's hours or time zone, so the dial
times are included for you to judge. Retry passes add to the same history,
which is what turns one unanswered call into a pattern.

### Retries

Two ways, for two kinds of failure.

**In the same run, for things that clear in minutes.** Set **Attempts per
business** above 1 and pick which outcomes to retry. Busy lines, technical
failures and our own agent going silent are selected by default; no answer,
voicemail, dropped calls, long holds, a pickup that never engaged, phone
menus that never reach a person and recorded announcements can be added. A
dropped call is retried only if nothing was answered before it dropped.
Attempts to the same business are spaced by **Wait before retrying**.

**When a business asks you to call back later**, it gets one callback in the
same run, at the time it named (or after 30 minutes), even with retries off.
If that time falls after the run's timeout, the row carries `callAfter`, and
a retry pass will not call it before then. "Don't call" is never called back.

**In a later run, for things that need hours.** Voicemail at 5pm usually
means "try tomorrow morning", not "try in two minutes". Pick the previous
run's dataset as the input, tick **Only call businesses not reached yet**,
and only the businesses worth another try are called, each keeping its full
attempt history. Every pass writes the **complete** picture: businesses it
did not call again are carried forward unchanged (marked `carriedForward`),
so the newest dataset always has every business and is the one the next pass
should pick. An Apify Schedule reuses the same saved input, so it would keep
reading the first run's dataset; pick the newest dataset for each pass
instead.

**Fit the retries inside the run's timeout.** A run stops at its **Timeout**
(Run options, 1 hour by default) and keeps using Apify compute while it waits
between attempts. No attempt starts unless it can finish before the timeout:
a business whose next attempt would run past it is not called again in this
run, keeps `retryable: true`, and its row says `run timeout` in
`laterAttemptNotPlaced` (or `reason`, if it was never called). The run log
warns at the start when your list or your retry settings will not fit. Raise
the Timeout, or finish with a retry pass. Waits longer than about 30 minutes
are usually cheaper as a later run than as an idle one.

What never gets retried, in the same run or a later one, whatever you select:
a business that answered (even if the call later dropped), a business that
hung up during or right after our introduction, a number answered by someone
who is not the business, and a call whose result we lost track of (it may
have succeeded). Calling back someone who just hung up is how numbers get
blocked.

Every attempt counts against **Maximum calls this run**, because PlaceCall's
daily limit counts attempts. First attempts always get the budget before any
retry, so a retry never costs a business its only call.

### Pricing

- **$0.10 for each call that reaches a real conversation** with someone at the
  business, whatever they answer, including a "no" or a call that drops
  mid-conversation. Charged through your Apify account; no separate sign-up
  and no API key.
- **Your first 10 such calls are free**, once per Apify account.
- **Free:** voicemail, no answer, busy lines, phone menus and recordings that
  never reach a person, hang-ups in the first seconds, wrong numbers, and our
  own errors. Retries of unanswered calls cost nothing.
- **"Check my list, don't call" is always free.**
- **On Apify's free plan**, you get the 10 free calls; after that, calling needs
  a paid Apify plan. This limit is set by PlaceCall, not by Apify.
- Your **maximum cost per run** (Run options) is respected: when it is reached,
  new calls stop, calls already in progress finish, and the rest come back as
  `notAttempted` with the reason.

Each row reports `costUsd` and `freeCallsUsed`; the run's status message sums
up free calls used and left, and what was charged. A row with
`chargePending: true` had a call that was not yet settled when the row was
written (a brief outage on our side), or whose charge was interrupted by a
restart and could not be confirmed, so its cost is not in the row. The
run's status message has the outcome: it counts the call if it was charged
later, and says so if it was never charged.

Apify compute is small: the Actor runs at 256 MB by default (512 MB when
**Maximum calls this run** is above 1,000), and a business with two attempts
used about 0.03 compute units in our tests. A run waiting between retries
still uses about 0.25 compute units an hour at 256 MB.

**Big lists and memory.** The Actor holds your whole list in memory,
including rows beyond **Maximum calls this run**. 256 MB covers about 2,000
Google Maps rows even with reviews included; 512 MB covers 5,000. If a
dataset is too big for the run's memory, the Actor stops within a few
hundred rows, before calling anyone, and tells you which **Memory** to set
under Run options.

### What you need to place calls

**Accept the PlaceCall terms** in the field at the top of the form (API:
`acceptTerms`, with the exact text of the option). By placing calls you agree
to the PlaceCall [API Terms](https://voygr.tech/terms-api) and
[Privacy Policy](https://voygr.tech/privacy-policy). When the terms change,
the option changes, and runs (including saved Tasks and Schedules) ask you to
accept again before any call.

### Limits worth knowing before a big run

- **Per Apify account per day:** up to 500 dial attempts on a paid Apify plan,
  30 on the free plan. Retries and unanswered calls count as attempts. The
  count resets at midnight UTC (5pm Pacific in summer, 4pm in winter). When it
  is reached, new calls stop and the rest come back as `notAttempted`; run
  again after the reset with **Only call businesses not reached yet**.
- **US destination numbers only.** English is the most reliable language.
- A call takes 60-90 seconds; the run log estimates how long your list will
  take and warns when it will not fit the run's **Timeout**.

### Writing good questions

Two or three short, factual questions per run. The agent asks one at a time
and a long opening is the main reason people hang up.

Ask for facts a person at the counter would know. "Are you open right now"
works; "what is your refund policy for orders placed through third-party
delivery apps" does not.

Nobody is watching an Actor run, so if a business asks something your
questions did not cover, there is no one to answer. Put any facts it might
need into **Answer if a call asks a question**. If that field is empty, the
agent is told at once to carry on without the answer rather than leaving the
business on hold. Every row reports what it was asked in
`questionsAskedMidCall`, so you can fold that into the next run.

### Good manners, enforced

Every call opens by identifying PlaceCall and stating that the line is
recorded. Asked directly whether they are speaking to a person, the agent
answers honestly and never claims to be human. Calls are recorded;
recordings and transcripts are kept for 90 days.

Only call businesses you have a legitimate reason to call, and only on behalf
of yourself or an organization you represent. These are real phones answered
by real people who did not ask to hear from you.

### Support

<https://github.com/voygr-tech/placecall>

# Actor input Schema

## `acceptTerms` (type: `string`):

By placing calls you agree to the PlaceCall <a href='https://voygr.tech/terms-api' target='_blank'>API Terms</a> and <a href='https://voygr.tech/privacy-policy' target='_blank'>Privacy Policy</a>. In short: call only on behalf of yourself or an organization you represent, and only for lawful purposes. Not needed for 'Check my list, don't call'.

## `locations` (type: `array`):

One object per business, e.g. \[{"name": "Corner Cafe", "phone": "(415) 555-0142"}]. A US phone number is required in any common format; a name is recommended. Every other field you include is passed through to the output so results join back to your own data. A picked dataset, a CSV file or link, or pasted rows below are used instead, in that order.

## `inputDatasetId` (type: `string`):

Use the output of a previous Actor run instead of pasting a list: another Actor's results, or a previous run of this one when retrying. When set, this wins over pasted businesses. Columns are detected automatically.

## `csvFile` (type: `string`):

Needs a column of US phone numbers. A business-name column helps: the agent says the name on the call. Every other column comes back with your results and can be used in questions as {column}. Upload a CSV or tab-separated file with a header row, or paste a link to one (a Google Sheet shared as 'Anyone with the link can view' works). Excel: save as CSV first.

## `pastedRows` (type: `string`):

Copy the cells from Google Sheets or Excel, header row included, and paste them here. Needs a column of US phone numbers; a business-name column helps.

## `questions` (type: `array`):

Asked of every business, one at a time. Two or three short factual questions work best. Use {column} to read a business its own data back, e.g. 'Is your address still {address}?' (any column from your list). Ready-made sets are in the README.

## `dryRun` (type: `boolean`):

Reads your list and shows what would happen: the columns detected, the exact questions for each business, rows that would be skipped, your free calls left and the most it could cost. Nobody is called and nothing is charged. Untick to place the calls.

## `template` (type: `string`):

API only: a named question set used when 'questions' is empty: verify\_hours, reservations, availability, new\_customers, confirm\_details. See the README.

## `templateVariables` (type: `object`):

API only: values for {placeholders} filled once for the whole run, e.g. {"item": "oat milk"}. A value here wins over a column of the same name.

## `context` (type: `string`):

Optional one-line framing, e.g. 'verifying directory information for a listings site'. Helps the agent sound purposeful rather than cold.

## `callerDisplayName` (type: `string`):

The name the agent gives. Recommended - without it the call introduces itself generically, which businesses trust less.

## `language` (type: `string`):

English is the most reliable; the rest are best-effort.

## `unattendedAnswer` (type: `string`):

Nobody is watching an Actor run, so if a business asks for a detail your questions did not cover, the agent has no one to ask. Put any facts it might need here. Every run also reports what it was asked.

## `maxCalls` (type: `integer`):

Upper bound on dials this run, retries included. The Actor also caps it at your key's own daily limit (25 on a free key, 5,000 after any purchase), which counts every attempt. First attempts get the budget before any retry, and businesses that never got a call come back as notAttempted with the reason.

## `maxWaitSecsPerCall` (type: `integer`):

How long to follow a single call before moving on. Most calls finish well inside 180s.

## `maxAttemptsPerBusiness` (type: `integer`):

1 means no retry. Retries only happen for the outcomes chosen below.

## `retryOn` (type: `array`):

Busy lines, technical failures and our own agent going silent are retried by default. A dropped call is retried only if nothing was answered before it dropped. A business that asks to be called back later always gets one callback, at the time it named. A business that hung up during or right after our introduction is never called again, whatever is selected here.

## `retryDelayMinutes` (type: `integer`):

Time between attempts to the same business. Calling back within a minute of a decline is the fastest way to get a number blocked. The run keeps running, and using Apify compute, while it waits, and every attempt must finish inside the run's Timeout (Run options, 1 hour by default): an attempt that would not is left for a later pass and its business comes back retryable.

## `onlyRetryFailures` (type: `boolean`):

When the dataset above is a previous run of this Actor, call only the businesses worth another try: not yet reached, not a hang-up during our introduction, not a call whose result was lost. Each row keeps its full attempt history. Each pass writes a new dataset, so the next pass should pick the newest one.

## `phoneField` (type: `string`):

Only needed if auto-detection picks the wrong column.

## `nameField` (type: `string`):

Only needed if auto-detection picks the wrong column for the business name.

## Actor input object example

```json
{
  "locations": [
    {
      "name": "Corner Cafe",
      "phone": "(415) 555-0142",
      "city": "San Francisco"
    },
    {
      "name": "Night Owl Diner",
      "phone": "(415) 555-0199",
      "city": "Oakland"
    }
  ],
  "questions": [
    "Are you open right now?",
    "What are your hours today?"
  ],
  "dryRun": true,
  "language": "en",
  "maxCalls": 100,
  "maxWaitSecsPerCall": 300,
  "maxAttemptsPerBusiness": 1,
  "retryOn": [
    "failed_busy",
    "failed_technical",
    "failed_agent_mute"
  ],
  "retryDelayMinutes": 5,
  "onlyRetryFailures": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `allFields` (type: `string`):

No description

## `termsAcceptance` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "locations": [
        {
            "name": "Corner Cafe",
            "phone": "(415) 555-0142",
            "city": "San Francisco"
        },
        {
            "name": "Night Owl Diner",
            "phone": "(415) 555-0199",
            "city": "Oakland"
        }
    ],
    "questions": [
        "Are you open right now?",
        "What are your hours today?"
    ],
    "dryRun": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("voygr/placecall").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "locations": [
        {
            "name": "Corner Cafe",
            "phone": "(415) 555-0142",
            "city": "San Francisco",
        },
        {
            "name": "Night Owl Diner",
            "phone": "(415) 555-0199",
            "city": "Oakland",
        },
    ],
    "questions": [
        "Are you open right now?",
        "What are your hours today?",
    ],
    "dryRun": True,
}

# Run the Actor and wait for it to finish
run = client.actor("voygr/placecall").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "locations": [
    {
      "name": "Corner Cafe",
      "phone": "(415) 555-0142",
      "city": "San Francisco"
    },
    {
      "name": "Night Owl Diner",
      "phone": "(415) 555-0199",
      "city": "Oakland"
    }
  ],
  "questions": [
    "Are you open right now?",
    "What are your hours today?"
  ],
  "dryRun": true
}' |
apify call voygr/placecall --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,voygr/placecall"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/rNDEpYebH29OGkTK1/builds/c3hcnya8N9oHd0Cx1/openapi.json
