> For the complete documentation index, see [llms.txt](https://docs.smsfunnel.com.br/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.smsfunnel.com.br/api/parceiros/ligacoes-broadcasts.md).

# Ligações - Broadcasts

Um **Broadcast de Voz** disca para **todos os leads de uma lista**, toca um áudio gravado e reporta o desfecho de cada ligação — atendeu, escutou até o fim, digitou uma tecla, não atendeu.

É o equivalente exato dos Broadcasts de SMS — mesmo nome e mesmo formato — e o mesmo recurso da tela **Broadcasts de Voz** do painel: o disparo criado pela API aparece lá, e vice-versa.

{% hint style="success" %}
**A URL de base de todos os endpoints é**: <https://web.smsfunnel.com.br/api/parceiros/v1>
{% endhint %}

{% hint style="info" %}
Os endpoints de listagem utilizam **paginação simples**. A resposta traz `current_page`, `data`, `per_page`, `next_page_url` e `prev_page_url` — não há campos `total` ou `last_page`. Para navegar, utilize o parâmetro de query `page`.
{% endhint %}

{% hint style="info" %}
Os endpoints ficam sob `/calls/`. O prefixo é do **canal**, não do fornecedor — os endpoints legados `/call4u/*` (ligação avulsa) continuam existindo e não mudaram.
{% endhint %}

**Fluxo completo**

```
1. POST /calls/audios                    → cria o áudio      (audio_id)
2. POST /lists                           → cria a lista      (list_id)
3. POST /lists/{id}/leads                → insere os telefones
4. POST /calls/broadcasts                → cria e dispara    (broadcast_id)
5. GET  /calls/broadcasts/{id}/contacts  → desfecho de cada ligação
```

Os telefones **sempre** vêm de uma Lista — não há inserção de números no corpo do disparo. O caminho da lista já normaliza o telefone, aplica a blacklist e é idempotente por `Idempotency-Key`, e a mesma lista pode alimentar uma campanha de SMS e um disparo de voz.

**Status possíveis**

**Status do broadcast:** `pending` (criado, importando os contatos), `processing` (discando), `paused` (pausado), `completed` (encerrado), `cancelled` (cancelado).

**Desfecho de cada ligação:** `waiting`, `no_answer`, `answered_hangup`, `answered_listened`, `answered_dtmf`, `cancelled` e `error` — a tabela completa está mais abaixo.

{% hint style="warning" %}
**Janela de silêncio: 23:00–07:30 (America/Sao\_Paulo).** Nada é discado nesse intervalo. Um `scheduled_date` que caia dentro dele é **reagendado para 07:30**, e não recusado — o valor devolvido na resposta é o horário efetivo.
{% endhint %}

**Créditos**

O custo é de **1 crédito de ligação a cada 30 segundos de áudio**, arredondado para cima, por ligação **atendida**. O valor por ligação é o `credits_per_call` do áudio.

{% hint style="info" %}
**Saldo parcial não impede a criação.** Apenas o saldo **zerado** retorna `402 INSUFFICIENT_CREDITS`. Ter menos crédito que o `estimated_credits` é aceito de propósito: quando o crédito acaba no meio do disparo, o contato volta para a fila em vez de se perder, e é discado novamente após a recarga.
{% endhint %}

***

#### Endpoints

<mark style="color:green;">**POST**</mark>**&#x20;/calls/broadcasts**

**Função:** Cria o disparo e inicia a importação dos telefones da lista.

**Corpo de envio:**

```json5
{
    "name": "Promoção de Agosto", // Obrigatório. De 3 a 100 caracteres.
    "lead_list_id": "0192e789-7b31-7c4e-9a1d-2f3b4c5d6e7f", // Obrigatório. Lista sua, com ao menos 1 lead.
    "audio_id": "0192f100-7b31-7c4e-9a1d-2f3b4c5d6e7f", // Obrigatório. Áudio seu e já processado ("ready": true).
    "max_attempts": 3, // Opcional. De 1 a 5. Padrão: 1. Tentativas por telefone quando não atende.
    "retry_interval_minutes": 60, // Opcional. De 1 a 120. Padrão: 30. Intervalo entre as tentativas.
    "scheduled_date": "2026-08-21T13:00:00-03:00", // Opcional. Data futura. Ausente = disca imediatamente.
    "scheduled_end_date": "2026-08-21T20:00:00-03:00", // Opcional. Fim da janela: nada é discado depois disso.
    "postback_url": "https://meu-sistema.com/webhooks/ligacoes" // Opcional. Até 2048 caracteres. Recebe o desfecho de cada ligação.
}
```

**Corpo de resposta — `201 Created`:**

```json5
{
    "id": "0192f200-7b31-7c4e-9a1d-2f3b4c5d6e7f", // ID do broadcast
    "name": "Promoção de Agosto",
    "status": "pending", // Nasce em pending e vai para processing quando a importação termina
    "lead_list_id": "0192e789-7b31-7c4e-9a1d-2f3b4c5d6e7f",
    "audio_id": "0192f100-7b31-7c4e-9a1d-2f3b4c5d6e7f",
    "max_attempts": 3,
    "retry_interval_minutes": 60,
    "scheduled_date": "2026-08-21T16:00:00+00:00", // Horário EFETIVO, já ajustado pela janela de silêncio
    "scheduled_end_date": "2026-08-21T23:00:00+00:00",
    "postback_url": "https://meu-sistema.com/webhooks/ligacoes",
    "total_contacts": 0, // Contatos já importados. Nasce 0: a importação é assíncrona
    "total_credits_used": 0, // Créditos efetivamente consumidos até agora
    "created_at": "2026-08-21T12:00:00+00:00",
    "updated_at": "2026-08-21T12:00:00+00:00",
    "estimated_credits": 1200, // Custo se TODOS os contatos forem discados e atenderem
    "credits_per_call": 2, // Créditos por ligação atendida (vem do áudio)
    "leads_count": 600 // Leads encontrados na lista no momento da criação
}
```

{% hint style="info" %}
`estimated_credits`, `credits_per_call` e `leads_count` aparecem **apenas nesta resposta de criação** — eles são conhecidos no momento do disparo. O custo real acumulado vive em `total_credits_used`, disponível em todas as consultas.
{% endhint %}

{% hint style="warning" %}
As validações são checadas **antes** de qualquer coisa ser gravada, nesta ordem:

1. Lista inexistente ou de outra conta → `404 NOT_FOUND`
2. Áudio inexistente ou de outra conta → `404 NOT_FOUND`
3. Áudio ainda em processamento → `422 AUDIO_NOT_READY` (com `details.audio_status`)
4. Lista sem nenhum lead → `422 EMPTY_LEAD_LIST`
5. Saldo de ligação zerado → `402 INSUFFICIENT_CREDITS`
   {% endhint %}

***

<mark style="color:green;">**GET**</mark>**&#x20;/calls/broadcasts**

**Função:** Lista os seus broadcasts de voz, do mais recente para o mais antigo.

**Parâmetros de query (opcionais):**

```json5
{
    "status": "processing", // Filtra por status: pending, processing, paused, completed, cancelled
    "start_date": "2026-08-01", // Filtra por created_at >= data (YYYY-MM-DD)
    "end_date": "2026-08-31", // Filtra por created_at <= data (YYYY-MM-DD)
    "text": "Promoção", // Busca parcial pelo nome do broadcast
    "per_page": 50, // Itens por página. Padrão: 50. Mínimo: 1, Máximo: 200.
    "page": 1 // Página desejada
}
```

**Corpo de resposta — `200 OK`:**

```json5
{
    "current_page": 1,
    "data": [
        {
            "id": "0192f200-7b31-7c4e-9a1d-2f3b4c5d6e7f",
            "name": "Promoção de Agosto",
            "status": "processing",
            "lead_list_id": "0192e789-7b31-7c4e-9a1d-2f3b4c5d6e7f",
            "audio_id": "0192f100-7b31-7c4e-9a1d-2f3b4c5d6e7f",
            "max_attempts": 3,
            "retry_interval_minutes": 60,
            "scheduled_date": "2026-08-21T16:00:00+00:00",
            "scheduled_end_date": null,
            "postback_url": "https://meu-sistema.com/webhooks/ligacoes",
            "total_contacts": 600, // Contatos importados
            "total_credits_used": 742, // Créditos consumidos até agora
            "created_at": "2026-08-21T12:00:00+00:00",
            "updated_at": "2026-08-21T16:41:02+00:00"
        }
    ],
    "first_page_url": "https://web.smsfunnel.com.br/api/parceiros/v1/calls/broadcasts?page=1",
    "from": 1,
    "next_page_url": null,
    "path": "https://web.smsfunnel.com.br/api/parceiros/v1/calls/broadcasts",
    "per_page": 50,
    "prev_page_url": null,
    "to": 1
}
```

{% hint style="info" %}
Um valor desconhecido em `?status=` é **ignorado** (a listagem vem completa), em vez de retornar erro ou uma lista vazia sem explicação.
{% endhint %}

***

<mark style="color:green;">**GET**</mark>**&#x20;/calls/broadcasts/{id}**

**Função:** Detalhe do broadcast, acrescido do bloco `report` — o mesmo recorte da tela de acompanhamento do painel.

**Corpo de resposta — `200 OK`:**

```json5
{
    "id": "0192f200-7b31-7c4e-9a1d-2f3b4c5d6e7f",
    "name": "Promoção de Agosto",
    "status": "processing",
    "lead_list_id": "0192e789-7b31-7c4e-9a1d-2f3b4c5d6e7f",
    "audio_id": "0192f100-7b31-7c4e-9a1d-2f3b4c5d6e7f",
    "max_attempts": 3,
    "retry_interval_minutes": 60,
    "scheduled_date": "2026-08-21T16:00:00+00:00",
    "scheduled_end_date": null,
    "postback_url": "https://meu-sistema.com/webhooks/ligacoes",
    "total_contacts": 600,
    "total_credits_used": 742,
    "created_at": "2026-08-21T12:00:00+00:00",
    "updated_at": "2026-08-21T16:41:02+00:00",
    "report": {
        "total_sent": 600, // Ligações despachadas
        "total_refused": 210, // Não atendeu, ocupado, congestionado ou indisponível
        "total_answered_hangup": 150, // Atendeu e desligou antes do fim do áudio
        "total_answered_listened": 180, // Atendeu e escutou 95% ou mais do áudio
        "total_answered_dtmf": 60, // Atendeu e digitou uma tecla
        "avg_duration_seconds": 21.4, // Duração média DAS ATENDIDAS
        "avg_listened_percentage": 71.83, // Percentual médio ouvido nas atendidas
        "total_credits_used": 742
    }
}
```

{% hint style="info" %}
O `report` não conta os contatos cancelados nem os que ainda estão na fila — por isso, num broadcast em andamento, a soma dos desfechos é menor que `total_contacts`.
{% endhint %}

***

<mark style="color:blue;">**PUT**</mark>**&#x20;/calls/broadcasts/{id}**

**Função:** Atualização parcial do broadcast. Envie apenas os campos que deseja alterar.

**Corpo de envio:**

```json5
{
    "name": "Promoção de Agosto — v2", // Opcional. De 3 a 100 caracteres.
    "audio_id": "0192f101-...", // Opcional. Áudio seu e já processado.
    "max_attempts": 2, // Opcional. De 1 a 5.
    "retry_interval_minutes": 45, // Opcional. De 1 a 120.
    "scheduled_date": "2026-08-22T09:00:00-03:00", // Opcional. Data futura.
    "scheduled_end_date": "2026-08-22T20:00:00-03:00", // Opcional. Posterior ao início efetivo.
    "postback_url": null // Opcional. Envie null para DESLIGAR o postback.
}
```

**Corpo de resposta — `200 OK`:** o broadcast atualizado, no mesmo formato da listagem (sem o bloco `report`).

{% hint style="warning" %}
**`lead_list_id` é proibido neste endpoint** (`422`). A lista já foi copiada para os contatos durante a importação, e trocá-la deixaria o broadcast apontando para uma lista que não corresponde ao que foi discado. Para outra lista, crie outro broadcast.
{% endhint %}

{% hint style="warning" %}
**A rediscagem só vale para importações futuras.** `max_attempts` e `retry_interval_minutes` são **copiados para cada contato** no momento da importação. Alterá-los depois que a importação terminou **não** reescreve os contatos já criados. É o mesmo comportamento do painel.
{% endhint %}

Broadcast já encerrado (`completed` ou `cancelled`) não pode ser editado → `409 INVALID_STATUS_TRANSITION`, com o status atual em `details.current_status`.

***

<mark style="color:blue;">**PUT**</mark>**&#x20;/calls/broadcasts/{id}/cancel**

**Função:** Cancela o broadcast. Permitido nos status `pending`, `processing` e `paused` — nos demais, `409 INVALID_STATUS_TRANSITION`.

**Corpo de resposta:**

* `200 OK` — broadcast `pending` ou `paused`: cancelamento completo (`"cancellation": "complete"`), nada estava no ar.
* `202 Accepted` — broadcast `processing`: cancelamento parcial (`"cancellation": "partial"`); ligações já publicadas no discador ainda podem completar.

```json5
{
    "id": "0192f200-7b31-7c4e-9a1d-2f3b4c5d6e7f",
    "name": "Promoção de Agosto",
    "status": "cancelled",
    "lead_list_id": "0192e789-...",
    "audio_id": "0192f100-...",
    "max_attempts": 3,
    "retry_interval_minutes": 60,
    "scheduled_date": "2026-08-21T16:00:00+00:00",
    "scheduled_end_date": null,
    "postback_url": "https://meu-sistema.com/webhooks/ligacoes",
    "total_contacts": 600,
    "total_credits_used": 742,
    "created_at": "2026-08-21T12:00:00+00:00",
    "updated_at": "2026-08-21T17:10:00+00:00",
    "cancellation": "partial" // "complete" (200) ou "partial" (202)
}
```

Os contatos ainda não discados recebem o desfecho `cancelled`; quem já foi discado mantém o desfecho original.

***

<mark style="color:blue;">**PUT**</mark>**&#x20;/calls/broadcasts/{id}/pause**

<mark style="color:blue;">**PUT**</mark>**&#x20;/calls/broadcasts/{id}/resume**

**Função:** Interrompe e retoma a discagem.

* `pause` aceita `pending` e `processing` → passa para `paused`.
* `resume` aceita apenas `paused` → volta para `processing`. Fora dessas transições, `409 INVALID_STATUS_TRANSITION` com o status atual em `details.current_status`.

**Corpo de resposta — `200 OK`:** o broadcast atualizado, no mesmo formato da listagem.

{% hint style="info" %}
Pausar **não** descarta contatos: o que faltava discar continua na fila e volta a ser processado no `resume`.
{% endhint %}

***

<mark style="color:green;">**GET**</mark>**&#x20;/calls/broadcasts/{id}/contacts**

**Função:** Lista os contatos do broadcast com o desfecho individual de cada ligação.

**Parâmetros de query (opcionais):**

```json5
{
    "status": "answered_dtmf", // Filtra pelo desfecho (ver tabela abaixo). Valor desconhecido é ignorado.
    "phone": "11999", // Busca parcial no telefone
    "per_page": 50, // Itens por página. Padrão: 50. Mínimo: 1, Máximo: 200.
    "page": 1 // Página desejada
}
```

**Corpo de resposta — `200 OK`:**

```json5
{
    "current_page": 1,
    "data": [
        {
            "id": "0192f300-7b31-7c4e-9a1d-2f3b4c5d6e7f", // ID do contato nesta tentativa
            "phone": "11999990001", // Telefone discado
            "name": "Ana Silva", // Nome do lead
            "status": "answered_dtmf", // Desfecho em slug estável — use este para programar
            "status_label": "Atendida com Digitação", // Texto em português exibido no painel
            "pabx_status": "ANSWER", // Retorno cru do discador
            "call_duration_seconds": 27, // Duração da ligação (nulo quando não atendida)
            "listened_percentage": 64.3, // Percentual do áudio ouvido (nulo quando não atendida)
            "dtmf_response": "1", // Tecla digitada (nulo quando não houve)
            "tries": 1, // Número desta tentativa
            "max_attempts": 3, // Tentativas contratadas para este contato
            "sent_at": "2026-08-21T16:02:11+00:00", // Momento do despacho para o discador
            "answered_at": "2026-08-21T16:02:39+00:00" // Momento em que o desfecho voltou do discador
        }
    ],
    "first_page_url": "https://web.smsfunnel.com.br/api/parceiros/v1/calls/broadcasts/0192f200-.../contacts?page=1",
    "from": 1,
    "next_page_url": null,
    "path": "https://web.smsfunnel.com.br/api/parceiros/v1/calls/broadcasts/0192f200-.../contacts",
    "per_page": 50,
    "prev_page_url": null,
    "to": 1
}
```

{% hint style="info" %}
Cada **tentativa** de rediscagem é uma linha própria, preservando o histórico. Um contato com `max_attempts: 3` que não atendeu duas vezes aparece em até três linhas, com `tries` 1, 2 e 3.
{% endhint %}

**Desfecho de cada ligação (`status`)**

| `status`           | Significado                                                                      |
| ------------------ | -------------------------------------------------------------------------------- |
| waiting            | Ainda não discado, ou aguardando a próxima tentativa                             |
| no\_answer         | Não atendeu, ocupado, congestionado ou número indisponível                       |
| answered\_hangup   | Atendeu e desligou antes do fim do áudio (ouviu menos de 95%)                    |
| answered\_listened | Atendeu e escutou 95% ou mais do áudio                                           |
| answered\_dtmf     | Atendeu e digitou uma tecla — a tecla está em `dtmf_response`                    |
| cancelled          | O broadcast foi cancelado antes deste contato ser discado                        |
| error              | Falhou antes de chegar ao discador (telefone fora do formato brasileiro, p. ex.) |

{% hint style="warning" %}
Use `status` para programar em cima do retorno. O `status_label` é o texto em português que o painel exibe e **pode mudar**.

`answered_at` é o momento em que o desfecho voltou do discador — não é o horário exato do atendimento.
{% endhint %}

***

#### Código de Erros Comuns

* `401 Unauthorized` (`UNAUTHORIZED`): Não foi possível identificar o parceiro a partir da Chave de API.
* `402 Payment Required` (`INSUFFICIENT_CREDITS`): Saldo de créditos de ligação zerado. Vêm em `details` o `estimated_credits` e o `available_credits`.
* `404 Not Found` (`NOT_FOUND`): O broadcast, a lista ou o áudio não foi encontrado ou não pertence à sua conta.
* `409 Conflict` (`INVALID_STATUS_TRANSITION`): O broadcast não aceita a operação no status atual (editar encerrado, cancelar encerrado, pausar pausado, retomar o que não está pausado). O status atual vem em `details.current_status`.
* `422 Unprocessable Entity` (`AUDIO_NOT_READY`): O áudio ainda não terminou de ser processado. O status dele vem em `details.audio_status`; aguarde `ready: true` em `GET /calls/audios/{id}`.
* `422 Unprocessable Entity` (`EMPTY_LEAD_LIST`): A lista informada não tem nenhum lead. Insira leads antes de criar o disparo.
* `422 Unprocessable Entity` (`EMPTY_LEAD_LIST`): A lista informada não tem nenhum lead. Insira leads antes de criar o disparo.
* `422 Unprocessable Entity` (`VALIDATION_ERROR`): Algum campo do corpo é inválido — inclui `lead_list_id` no `PUT`, `scheduled_date` no passado, `scheduled_end_date` anterior ao início efetivo e `postback_url` apontando para endereço privado ou interno.
* `429 Too Many Requests` (`RATE_LIMIT_EXCEEDED`): Limite de **60 requisições por minuto** excedido. Consulte o header `Retry-After`.

**Corpo de resposta de erro:**

```json5
{
    "error": {
        "code": "AUDIO_NOT_READY", // Código do erro
        "message": "O áudio ainda não terminou de ser processado. Consulte GET /parceiros/v1/calls/audios e aguarde `ready: true`.", 
// Descrição
        "details": { // Opcional: detalhes adicionais
            "audio_status": "processing"
        }
    }
}
```

**Dúvidas?**

Em caso de dúvidas, entre em contato com nosso suporte.
