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

# Salesforce lead push

> Push Salesforce Leads to Yotel for AI-agent dialing, and get call outcomes back as Call_Detail__c records.

This is the hand-over page for a server-to-server integration: your
Salesforce org (or the automation in front of it) pushes Leads into a
Yotel campaign via the public API, Yotel's voice agent calls them, and
Yotel writes the outcome back as a `Call_Detail__c` record on the Lead.

<Info>
  This is a different integration from the [Yotel for Salesforce managed
  package](/integrations/salesforce), which is a Lightning Web Component
  for a human to click "Push to Yotel" from inside the CRM. This page
  covers the API-driven path — no LWC, no browser click, your own
  automation calls the REST API directly (or bulk-pushes a report/list
  view export on a schedule).
</Info>

## 1. Mint an API key

1. Log into `app.yotel.in` as a tenant admin (`api_keys:manage`
   permission).
2. **Settings → API Keys** (`app.yotel.in/settings/api-keys`) →
   **Generate Key**.
3. Pick the **live** environment. For scopes, either leave everything
   unticked (full access) or tick at least `leads:write` — the scope
   this integration pushes leads with.
4. Copy the `yt_live_…` value immediately — it's shown once; Yotel
   stores only a hash.

Use a `yt_test_…` key first if you want to validate the integration
without dialing the PSTN — see [Quickstart](/quickstart) and
[Authentication](/authentication).

## 2. Push leads

Push a Salesforce Lead using its own field names — Yotel accepts
`lead_id` / `first_name` / `mobile` as aliases for our canonical
`salesforce_lead_id` / `name` / `phone`, so there's no field-mapping
step on your side. Full reference: [Leads](/api-reference/leads).

<Info>
  **This is the same endpoint the [Leads](/api-reference/leads) reference
  documents**, called with the CRM spellings. The two pages are not two
  APIs. Either spelling works on every request; if you send both, the
  canonical name wins. Responses always use the canonical names.
</Info>

<CodeGroup>
  ```bash Single lead theme={null}
  curl -X POST https://api.yotel.in/api/v1/campaigns/{campaign_id}/leads \
    -H "Authorization: Bearer yt_live_YOUR_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "lead_id": "00Qfv00000NZA2UEAX",
      "first_name": "Priya Sharma",
      "mobile": "9876543210"
    }'
  ```

  ```bash Bulk (up to 500) theme={null}
  curl -X POST https://api.yotel.in/api/v1/campaigns/{campaign_id}/leads:bulk \
    -H "Authorization: Bearer yt_live_YOUR_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: 8a4c1f3e-9c21-4b7d-8e8a-9b1e2f3a4c5d" \
    -d '{
      "leads": [
        {"lead_id": "00Qfv00000NZA2UEAX", "first_name": "Priya Sharma", "mobile": "9876543210"},
        {"lead_id": "00Qfv00000NZA2UEAY", "first_name": "Raj Patel", "mobile": "9123456789"}
      ]
    }'
  ```
</CodeGroup>

<Tip>
  Send `Idempotency-Key` on every push, single or bulk. It's the correct
  tool for a **retry** of the same request — a batch export job that
  times out and resends the identical payload gets back the original
  response, no reprocessing, no duplicate. It is not a dedup mechanism
  for two genuinely different pushes of the same lead — see below for
  that. Details: [Idempotency](/concepts/idempotency).
</Tip>

`lead_id` (`salesforce_lead_id`) does two jobs beyond round-tripping to
`Call_Detail__c.Lead__c` later: it's a **second dedup key** alongside the
phone number, so a re-push of the same Lead with a corrected number is
still recognized as the same lead rather than double-dialed, and it's
how Yotel resolves the Lead when writing the call outcome back (§4).

**Single vs. bulk handle a duplicate differently.** A single push
(`POST /leads`) that matches an existing lead in the campaign — by phone
hash or by `salesforce_lead_id`, either is enough — is **rejected
outright** with `409 Conflict`:

```json theme={null}
{
  "detail": "Duplicate lead in campaign",
  "id": "lead_abc",
  "lead_id": "lead_abc",
  "salesforce_lead_id": "00Qfv00000NZA2UEAX"
}
```

`id` there is the id of the **existing** Yotel lead (not the one you
just tried to push); `salesforce_lead_id` is its existing SF id, or
`null`. No phone number appears in the request or the 409 body.

<Warning>
  Do **not** join on `lead_id` here. In a request body — and in every
  bulk `results[]` row — `lead_id` is your *Salesforce* Lead id. In this
  409 body it is *Yotel's* lead id. Read **`id`** for the Yotel lead and
  `salesforce_lead_id` for yours; `lead_id` is kept only for backwards
  compatibility. See [Leads](/api-reference/leads#duplicate-lead-409).
</Warning>

A bulk push (`POST /leads:bulk`) never 409s for this reason — a
duplicate row is counted in `duplicates` and reported as
`{"lead_id": ..., "phone": ..., "status": "duplicate"}` in `results`
alongside every other row, so one bad row in a 500-lead file doesn't
fail the batch. Full shapes for both: [Leads](/api-reference/leads).

## 3. What happens on the call

Once the lead is pushed, Yotel's dialer places the call and bridges the
answered leg into the configured voice agent. `name` (`first_name`) you
pushed is forwarded automatically as a `{{lead_name}}` prompt variable —
so if your agent's greeting template says *"Hi {{lead_name}}, this is…"*,
the bot greets the caller by the name you sent, no extra config needed.
`salesforce_lead_id` is forwarded the same way, in case your agent's
prompt or tool-calls need it mid-conversation.

<Note>
  Personalization is additive: if you also push
  `"metadata": {"variables": {...}}` with your own keys, those are
  forwarded too and take precedence over the `lead_name` /
  `salesforce_lead_id` defaults for any key you set explicitly.
</Note>

## 4. Call outcomes in Salesforce

When the call ends, Yotel writes a `Call_Detail__c` record (create, or a
PATCH if one already exists for that call — writeback is idempotent, so
a retried delivery never duplicates the record):

| `Call_Detail__c` field | Value |
| - | - |
| `Lead__c` | The Salesforce Lead resolved from `lead_id` |
| `Call_ID__c` | Yotel's call id (idempotency key for the writeback itself) |
| `Call_Type__c` | `Outbound` |
| `Status__c` | `Call Complete` if answered; otherwise `BUSY` / `NO ANSWER` / `MISSED` depending on hangup cause (a carrier no-answer is never reported as a bad number) |
| `Disposition__c` | `Contacted` if answered, else `Not Contacted` |
| `Sub_Dispoisition__c` *(spelt that way in your org's schema)* | Mapped from Yotel's disposition where a picklist entry exists (`Not Interested`, `Call Back`, `Interested – Details Shared`, `Site Visit Scheduled`, `Qualified`, `Do not Call`, `Invalid Number`, `No Answer`); left **unset** rather than guessing when there's no match, so a novel disposition never fails the record |
| `Duration__c` | Talk time in seconds (`0` if unanswered) |
| `Start_Time__c` / `End_Time__c` | Answer time (or dial time if never answered) / end time |
| `Recording_File__c` | The [stable recording link](#5-subscribe-to-call-outcomes) — set once available, which is usually after the initial create |
| `Comments__c` | Agent notes, or the AI call summary if no notes were left |
| `Dialer_Dispo__c` | The raw Yotel disposition string, unmapped — nothing is lost even when `Sub_Dispoisition__c` couldn't be set |
| `Dialer_Source__c` | `Yotel` |
| `Source_Of_Call__c` | `Zetta AI` |

<Warning>
  **No phone number is ever written to `Call_Detail__c`.** The field map
  is an explicit allowlist — Yotel does not have your customer's number
  to write even if it wanted to (see the privacy section below).
</Warning>

**The one thing your Salesforce admin needs to do:** grant the
connected app's integration user **Read, Create, and Edit** on
`Call_Detail__c` — not Create alone — plus edit **field-level security**
on the fields above. Yotel needs all three because writeback isn't a
one-shot create: before writing, it **queries** `Call_Detail__c` by
`Call_ID__c` to check whether a record already exists for this call
(idempotency — Read), and after the initial create it **PATCHes** the
same record once the recording finishes processing, typically a minute
or two later (`Recording_File__c` — Edit), plus on any retry of a
transient failure (Edit again). Read on `Lead` is also required, but
that's standard — your integration user already has it. Yotel's account
team configures the connected-app credentials on our side during
onboarding — the SF-side grant above is the only action item on yours.

<Warning>
  **Create-only is not enough.** With Create but not Edit, the initial
  `Call_Detail__c` record is written successfully, but the follow-up
  recording-link PATCH fails with Salesforce's `INSUFFICIENT_ACCESS`
  error — every call outcome shows up in Salesforce, permanently missing
  its recording link, and the failure is easy to miss because the first
  write looked fine. Grant Edit up front rather than debugging this
  later.
</Warning>

Until the full grant is in place, writeback attempts fail visibly (not
silently): they show up as failed jobs your Yotel account team can see,
and the call itself, the recording, and the `call.ended` webhook (§5)
are completely unaffected — call outcomes just don't reach Salesforce
until the permission lands.

## 5. Subscribe to call outcomes

If you want to write your own records instead of (or in addition to)
relying on the `Call_Detail__c` writeback, subscribe to the webhooks
directly:

* **`call.ended`** — fires the moment a call finishes, before the
  recording is ready. Carries `call_id`, `salesforce_lead_id`,
  `status`, `hangup_cause`, durations, `disposition`, and a `recording`
  object (`available: false` at this point).
* **`call.disposition_set`** — fires when the *behavioural* outcome is
  recorded (Interested, Site Visit, Do Not Call), which is what lands in
  `Sub_Dispoisition__c`. It also fires again on a later correction,
  carrying `previous_disposition` — so if you keep your own copy of the
  outcome, this is the event that keeps it right rather than leaving you
  with the first answer forever.
* **`call.recording_ready`** — fires 30–120s later with
  `recording.available: true` and the same non-expiring
  `recording.url` that Yotel writes to `Recording_File__c`.

Which of these reach you, and which of them contribute to the
`Call_Detail__c` writeback, is configurable per campaign — see
[Event routing](/webhooks/event-routing).

Full payload shapes: [Event catalog](/webhooks/event-catalog). Set up
the subscription at **Developer → Webhooks** and verify deliveries with
[Signature verification](/webhooks/signature-verification) — every
delivery is signed so you can confirm it came from Yotel before trusting
the body.

<Note>
  `call.ended` carries **no phone number and no lead name** — join on
  `lead_id` or `salesforce_lead_id` against your own Lead record for
  contact details. This is deliberate: it's what keeps the event safe to
  log and safe to deliver regardless of your tenant's privacy mode.
</Note>

## 6. Privacy

If your tenant is configured for strict PII handling (the default we
set up for a Salesforce-integrated tenant, since the number's system of
record is your CRM, not ours):

* The phone number is stored **encrypted at rest** from the moment you
  push it. It's decrypted in memory only for the instant it takes to
  place the call, and never written back to the database in plaintext.
* Every API response you receive — the single-push response, every row
  of a bulk response, `GET`/list calls — echoes a **masked** number
  (`••••3210`) instead of the plaintext, for the lifetime of the lead.
* The number is **purged from the database entirely** — enforced at the
  database layer, not by a code path that could be skipped. A lead that
  opts out (DND) is purged the moment it does. A lead that finishes
  (completed, or exhausted its retries) keeps its number for the
  tenant's retention window (default 30 days, configurable per tenant)
  so the campaign can be re-churned or cloned, and is purged by a
  nightly sweep after that.
* The number is **never written to application logs**, on any call
  path.
* The voice agent receives the number **for the duration of the call
  only**, so it can send a WhatsApp message to the person it is speaking
  with. The Zetta voice agent holds it in memory, never stores it, and
  masks it (last 4 digits) in every log and in its post-call report. For
  personalization it uses `lead_name` / `salesforce_lead_id` (§3), not
  the number.

This is not configurable per-request — it's a tenant-level setting
Yotel sets up during onboarding, so every integration on your tenant
gets the same guarantee automatically.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.