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

# API - VoIP Webhook

Rota pública usada por centrais/provedores de telefonia externos para enviar chamadas já finalizadas ao SprintHub.

A cada evento recebido, o SprintHub:

* identifica o lead pelo telefone (ou cria um novo lead, se o número for desconhecido);
* registra a chamada no histórico de chamadas (`voip_history`), visível no CRM;
* registra o evento na linha do tempo do lead (`voip.call_incoming` ou `voip.call_outgoing`).

<figure><img src="https://3534414179-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F9FzUKkagBNy5uQ7AtGNa%2Fuploads%2FjXbSoCnD5GQmiJzMJ4Xf%2Fimage.png?alt=media&amp;token=9673d5ec-fa01-4b76-9a14-e6aa88e22ef5" alt=""><figcaption></figcaption></figure>

***

### Endpoint

```
POST https://api.sprinthub.app/voip/webhook?i=NOME_DO_CLIENTE
Content-Type: application/json
```

Substitua `NOME_DO_CLIENTE` pelo nome da instância (ex: `meucliente`). O parâmetro `?i=` é o que identifica a base de dados do cliente e deve estar sempre presente na URL configurada no provedor.

***

### Autenticação

A autenticação é **opcional** e definida por instância:

| Situação                            | Comportamento                                                         |
| ----------------------------------- | --------------------------------------------------------------------- |
| Cliente **sem** segredo configurado | O endpoint aceita a requisição sem validação adicional                |
| Cliente **com** segredo configurado | É obrigatório enviar o segredo, senão a resposta é `401 unauthorized` |

Quando o segredo estiver ativo, envie-o de uma das duas formas:

```
x-voip-signature: SEU_SEGREDO
```

ou

```
POST /voip/webhook?i=NOME_DO_CLIENTE&secret=SEU_SEGREDO
```

> A configuração do segredo (`voip_webhook_secret`) é feita pela equipe SprintHub na instância do cliente.

***

### Corpo da requisição

O corpo é um envelope com dois campos: `event` e `data`.

```json
{
  "event": "call",
  "data": { ... }
}
```

| Campo   | Tipo   | Obrigatório | Descrição                                     |
| ------- | ------ | ----------- | --------------------------------------------- |
| `event` | string | sim         | Único valor aceito hoje: `"call"`             |
| `data`  | object | sim         | Dados da chamada finalizada (tabela a seguir) |

#### Campos de `data`

| Campo         | Tipo    | Obrigatório | Descrição                                                                       |
| ------------- | ------- | ----------- | ------------------------------------------------------------------------------- |
| `user`        | number  | **sim**     | ID do usuário SprintHub que fez/recebeu a chamada. Precisa existir na instância |
| `leadPhone`   | string  | **sim**     | Telefone do contato, com DDI. Caracteres não numéricos são removidos            |
| `type`        | string  | **sim**     | `INCOMING` (recebida) ou `OUTGOING` (realizada)                                 |
| `status`      | string  | **sim**     | `COMPLETE` (atendida) ou `INCOMPLETE` (não atendida)                            |
| `duration`    | number  | **sim**     | Duração em **segundos**. Use `0` para chamadas não atendidas                    |
| `record`      | boolean | **sim**     | Indica se a chamada foi gravada na central                                      |
| `service`     | string  | **sim**     | Identificador do provedor/central (até 32 caracteres). Ex: `"twilio"`           |
| `lead`        | number  | não         | ID do lead, se já conhecido. Se omitido, o lead é resolvido pelo telefone       |
| `leadName`    | string  | não         | Nome do contato (até 64 caracteres)                                             |
| `userName`    | string  | não         | Nome do operador (até 64 caracteres)                                            |
| `callId`      | string  | não         | ID da chamada no provedor (até 64 caracteres). Usado para conciliação           |
| `description` | string  | não         | Observação livre sobre a chamada (até 1000 caracteres)                          |

> Campos não listados acima são rejeitados: qualquer propriedade extra resulta em `400 invalid_body`.

#### Exemplo — chamada recebida e atendida

```json
{
  "event": "call",
  "data": {
    "user": 42,
    "userName": "Maria Souza",
    "leadPhone": "+55 11 98888-7777",
    "leadName": "João Silva",
    "type": "INCOMING",
    "status": "COMPLETE",
    "duration": 185,
    "record": true,
    "callId": "CA-2f9c81",
    "service": "twilio",
    "description": "Cliente pediu proposta comercial"
  }
}
```

#### Exemplo — chamada realizada e não atendida

```json
{
  "event": "call",
  "data": {
    "user": 42,
    "leadPhone": "5511988887777",
    "type": "OUTGOING",
    "status": "INCOMPLETE",
    "duration": 0,
    "record": false,
    "callId": "CA-2f9c82",
    "service": "twilio"
  }
}
```

#### Exemplo com cURL

```bash
curl -X POST "https://api.sprinthub.app/voip/webhook?i=meucliente" \
  -H "Content-Type: application/json" \
  -H "x-voip-signature: SEU_SEGREDO" \
  -d '{
    "event": "call",
    "data": {
      "user": 42,
      "leadPhone": "5511988887777",
      "type": "INCOMING",
      "status": "COMPLETE",
      "duration": 185,
      "record": true,
      "service": "twilio"
    }
  }'
```

***

### Collection do Postman

Arquivo: `voip_webhook_postman_collection.json` — importe no Postman em *Import > File*.

Antes de rodar, ajuste as variáveis da collection:

| Variável    | Descrição                                                      |
| ----------- | -------------------------------------------------------------- |
| `baseUrl`   | URL da API (ex: `https://api.sprinthub.app`)                   |
| `instance`  | Nome da instância, usado no `?i=`                              |
| `secret`    | Segredo do webhook. Deixe vazio se a instância não usa segredo |
| `userId`    | ID de um usuário existente na instância                        |
| `leadPhone` | Telefone usado nos testes                                      |
| `service`   | Identificador do provedor enviado em `service`                 |
| `leadId`    | Preencha apenas para a requisição *"com lead informado"*       |

A collection tem duas pastas:

* **Sucesso** — chamada recebida atendida, realizada não atendida, payload mínimo, com lead informado e com telefone mascarado. Cada requisição valida `201`/`msg: created` e guarda o ID retornado na variável `historyId`.
* **Erros** — os casos de `400` (campo obrigatório ausente, `event` inválido, enum inválido, campo extra) e o `401` de segredo inválido.

> As requisições da pasta **Sucesso** criam registros reais no histórico de chamadas e podem criar o lead pelo telefone informado. Use uma instância de teste. O `callId` é gerado com `{{$timestamp}}` para cada envio ser único.

***

### O que o SprintHub faz com o evento

1. **Valida o corpo** — formato, tipos e campos obrigatórios.
2. **Normaliza o telefone** — remove tudo que não é dígito de `leadPhone`.
3. **Resolve o lead**:
   * se `lead` foi enviado, ele é usado direto;
   * senão, procura um lead existente pelos campos telefone, celular e WhatsApp (com tratamento do 9º dígito em números brasileiros);
   * se não encontrar, cria um novo lead com origem **VoIP**, usando o nome e a foto do WhatsApp quando o número tiver conta ativa, ou o próprio telefone como nome.
4. **Registra a chamada** no histórico de chamadas e retorna o ID gerado.
5. **Registra na linha do tempo do lead** o evento `voip.call_incoming` ou `voip.call_outgoing`, com operador, telefone, status, duração, `callId` e `service`.

> Se a etapa de resolução/criação do lead falhar, a chamada ainda é registrada — apenas sem vínculo com lead (e, nesse caso, sem evento na linha do tempo).

***

### Respostas

| Código | Corpo                                            | Significado                                                           |
| ------ | ------------------------------------------------ | --------------------------------------------------------------------- |
| `201`  | `{ "msg": "created", "id": 123 }`                | Chamada registrada. `id` é o identificador no histórico de chamadas   |
| `400`  | `{ "msg": "invalid_body", "err": { ... } }`      | Corpo inválido: campo obrigatório ausente, tipo errado ou campo extra |
| `401`  | `{ "msg": "unauthorized" }`                      | Segredo configurado na instância e não enviado (ou enviado incorreto) |
| `500`  | `{ "msg": "failed_voip_webhook", "err": "..." }` | Falha ao gravar a chamada. Pode ser reenviado                         |

O campo `err` do `400` traz o detalhe da validação, útil para depurar a integração:

```json
{
  "msg": "invalid_body",
  "err": {
    "instancePath": "/data",
    "keyword": "required",
    "message": "must have required property 'service'"
  }
}
```

***

### Boas práticas

* **Envie um evento por chamada finalizada**, após o encerramento — o webhook não trata eventos de progresso (ringing, answered, etc.).
* **Não há deduplicação por `callId`**: reenviar o mesmo evento cria um novo registro no histórico. Envie sempre `callId` para permitir conciliação e evite reenvios desnecessários.
* **Reenvie apenas em erro `500` ou timeout**, com espera progressiva entre tentativas. Como a falha pode ocorrer após a gravação da chamada, um reenvio pode gerar registro duplicado — use o `callId` para identificar.
* **`duration` em segundos**, sempre número inteiro.
* **`record: true` apenas sinaliza** que existe gravação na central; o áudio não é enviado nem anexado por este endpoint.
* **Respeite os limites de tamanho** dos campos de texto (`userName`, `leadName` e `callId` até 64; `service` até 32; `description` até 1000). Valores maiores podem ser truncados ou causar erro `500`.
* **Automação:** o gatilho *"recebeu ligação"* (`leadPhoneCall`, evento `received`) reconhece as chamadas `INCOMING` recebidas por este webhook. O filtro por número do operador não se aplica, pois o webhook não envia esse número.
* Este endpoint **não gera consumo de créditos nem transcrição automática** — ele apenas registra o histórico da chamada e o evento no lead.
