> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upliftai.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Getting started with batch calling

Give us a **list of numbers**, an **assistant**, and a **goal**. We call everyone on the list, retry the ones we missed, and hand back what happened, per person. That's a **campaign**, and it's how most of our customers use Uplift: order confirmations, payment reminders, event invitations, lead qualification.

## What you'd build yourself, and what a campaign does instead

You can dial a list with the [calling API](/api-reference/starting-a-conversation/dispatch-a-call-from-this-assistant) alone, and plenty of people start there. Here's what we take over when you use a campaign:

| With the calling API, you                                 | With a campaign, we                                                      |
| --------------------------------------------------------- | ------------------------------------------------------------------------ |
| Loop over the list, and back off when your line is busy   | Keep a **queue** and dial as lines free up                               |
| Decide when it's too late to call                         | Dial inside your **calling window** and hold the rest for tomorrow       |
| Redial the busy ones and remember who is on attempt three | **Retry by outcome**, with a backoff, three attempts by default          |
| Put each customer's details into the call yourself        | Put each contact's **row from the sheet** in front of the model          |
| Work out what the call achieved from the transcript       | Hand the agent a **goal tool** that records the outcome in a fixed shape |
| Collect results per person from session reads             | Keep **results, stats, and a callbacks queue** per contact               |
| Handle the 402 when credits run out                       | **Pause** the campaign until you top up                                  |
| Stop and restart your loop                                | **Pause, resume, schedule, or close** it, one request each               |

Everything else is the same [call](/voice-agents/concepts/how-it-works): the same assistant, the same session record, the same four [webhooks](/voice-agents/webhooks/overview), with a `campaignId` on each one.

## Your first campaign

All of this is in the [portal](https://upliftai.org/app/calling) too. Over the API it's five requests. Fill in a draft in any order and [update](/api-reference/campaigns/update-a-campaign) it section by section. Each update sends the whole section. Launch rejects a draft with no assistant, or a static one with no list. It doesn't check the goal or the window, so set both first.

<Steps>
  <Step title="Make the contact list">
    [Create a contact list](/api-reference/contacts/create-a-contact-list) from pasted rows, an uploaded file, or a Google Sheet. We drop bad numbers, collapse duplicates by number, and tell you how many made it. Keep the `listId`.

    ```bash theme={null}
    curl -X POST https://api.upliftai.org/v1/contact-lists \
      -H "Authorization: Bearer $UPLIFT_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "August dues",
        "kind": "paste",
        "data": "عائشہ صدیقی, +923001234567\nبلال احمد, 03214567890"
      }'
    ```
  </Step>

  <Step title="Create the campaign">
    [Create a campaign](/api-reference/campaigns/create-a-campaign) with `mode: bounded`, the assistant that dials, the goal, the list, a calling window, and a retry policy.

    ```json A payment reminder run, one line, three attempts highlight={8,10-15} theme={null}
    {
      "name": "August dues",
      "mode": "bounded",
      "config": {
        "goal": "payment_reminder",
        "assistant": { "assistantId": "80b00435-xxxx-xxxx-xxxx-xxxxxxxxxxxx" },
        "contacts": { "listId": "4e2c8d61-xxxx-xxxx-xxxx-xxxxxxxxxxxx" },
        "schedule": { "callingWindow": { "from": "10:00", "to": "19:00", "tz": "Asia/Karachi" } },
        "dialing": {
          "lines": 1,
          "retryPolicy": {
            "maxAttempts": 3,
            "retryOn": ["no_answer", "busy", "voicemail", "unreachable"],
            "backoffMinutes": [15, 120]
          }
        }
      }
    }
    ```

    The **goal** picks which tool the agent uses to record the outcome. A payment reminder gets `record_payment_promise`, an order confirmation gets `record_order_confirmation`. [Goals & scorecards](/voice-agents/campaigns/goals-and-scorecards) has the rest. Send all three fields of the **retry policy**, or none. We ignore a partial one and apply the defaults: three attempts, ten minutes and then an hour apart. They retry on no answer, busy, voicemail, silent pickups, unreachable, and failed dials. Leave `lines` at 1 until your account has more. [Scheduling & retries](/voice-agents/campaigns/scheduling-and-retries) covers the details.

    <Warning>
      **No calling window means we dial around the clock.** Set one before you launch.
    </Warning>
  </Step>

  <Step title="Add a scorecard">
    Optional for launch, and I recommend it. A scorecard is a set of rules that add up to 100 points, and we grade every connected call against it. The grade is the score and sentiment on the calls list. Without one we grade nothing. Today that means the results list stays empty and only `call.completed` fires. [Scorecards](/voice-agents/assistants/scorecards) shows how to write one. The scorecard goes on `config.scorecard`, on create or in an update.
  </Step>

  <Step title="Launch">
    [Launch now](/api-reference/campaigns/launch-a-campaign-now), or [schedule](/api-reference/campaigns/schedule-a-campaign-launch) a time. Launch **copies the list into the campaign**, so rows added to the list afterwards never dial.
  </Step>

  <Step title="Watch it, then read the results">
    [Get the campaign](/api-reference/campaigns/get-a-campaign) for `state`. [List its calls](/api-reference/campaign-outcomes/list-a-campaigns-calls) for one row per contact, dialed or not, with the attempts so far, the next due time, and the score once graded. Add `?include_summary=true` and each row carries its call's summary. When the list runs out the campaign completes on its own.

    [Results](/api-reference/campaign-outcomes/list-a-campaigns-outcomes) has one row per call with a recorded outcome: a promise to pay, a booking, a qualified lead. [Stats](/api-reference/campaign-outcomes/aggregate-stats-for-a-campaign) has the totals. Each call's `callId` is a session id, so percent-encode it and [read the call in full](/voice-agents/calling/session-details). Or don't poll at all: the [webhooks](/voice-agents/webhooks/events) deliver each call as it lands.
  </Step>
</Steps>

## Where your contacts come from

Three ways in, and all of them end in a `listId`:

* **Paste rows.** Put a name and a number per line straight in the create request, like the example above.
* **Upload a file.** Send a CSV or an Excel `.xlsx` file, check the column mapping we detected, then create the list from the `uploadId`.
* **Link a Google Sheet.** Share the link, or connect a private sheet through Google. We pull it once into a list.

A row needs a **name** and a **phone number**. Every other column goes to the model with the call: order number, amount due, crop, whatever the call needs. Type the values the way you want them said. [Contacts](/voice-agents/campaigns/contacts) covers the column rules, and [Variables](/voice-agents/personalization/variables) shows exactly what the model sees.

## Static or continuous

`mode` is fixed at creation. It's the one thing a draft can't change.

* **Static**, `bounded` in the API, dials a list you have today and completes when the list is done. This month's dues. Make the list, create, launch or schedule, read the results.
* **Continuous** takes contacts for as long as it runs and ends when you close it. Every new order. Create, launch with nothing, append as events happen, close.

[Static vs continuous campaigns](/voice-agents/campaigns/static-vs-continuous) has the full comparison, including what each mode lets you change after launch.

## Continuous, driven by your system

This is the same campaign, except your system feeds it. Create it with `mode: continuous` and no list, then launch it. It sits in `running` with nothing to do until you send work. On every event on your side, [append contacts](/api-reference/campaigns/append-contacts-to-a-campaign):

```bash A new order, appended the moment it's placed theme={null}
curl -X POST https://api.upliftai.org/v1/campaigns/$CAMPAIGN_ID/contacts \
  -H "Authorization: Bearer $UPLIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "externalRef": "ord-48812",
        "name": "عائشہ صدیقی",
        "phone": "+923001234567",
        "attributes": { "order": "48812", "amount": "Rs 4,500", "city": "لاہور" }
      }
    ]
  }'
```

* **Send an `externalRef`.** Your order id. We tell contacts apart by it. Send the same ref while the call is still pending and nothing changes. Send it again once the contact is done and we queue a fresh call, so a reorder next week works. Without it, each request enrolls the number again.
* **Append up to a thousand at a time.** The contacts dial right away, inside the window. We skip a bad row, and the response says which and why.
* **Close it when the source dries up.** [Close](/api-reference/campaigns/close-a-continuous-campaign) stops new contacts, lets the queue and the retries finish, and completes. There's no undo.

Every webhook carries the `campaignId` and the call id embeds the contact, so your endpoint can tie a `call.completed` back to the order that caused it. That's the whole loop: order placed, append, call, result.

## Tech details for techies

* **One line, one call at a time.** The dialer looks for work every five seconds and dials as lines free up. On one line, figure a few dozen attempts an hour. An unanswered dial frees the line in about a minute. A conversation holds it for the call plus grading. A list of three hundred is a day or more once you count retries. [Phone numbers & concurrency](/voice-agents/calling/concurrency) is where more lines come from.
* **Out of credits pauses the campaign.** `statusMessage` says so. Top up, then [resume](/api-reference/campaigns/resume-a-paused-campaign).
* **A deleted assistant pauses it too.** `statusMessage` reads `Paused: assistant version not found`, and that attempt counts. Once a campaign is scheduled or launched its assistant can't change, so start a new campaign.
* **A campaign dials the assistant's published version.** Publish, and the next dial uses the new one. To hold a campaign on one version, set `config.assistantVersion` to its number — see [Draft and publish](/voice-agents/assistants/versions).
* **Pause stops new dials only.** Calls already up finish and get graded. Retries keep their times and go out after resume.
* **A campaign runs once.** When it completes there's no relaunch. Next month's dues are a new campaign.
* **Stats sample the thousand most recently touched contacts.** The call counters are exact, but the contact counts (attempted, retrying, exhausted) stop there. For a bigger campaign, page through the calls list.
