> 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/en/topics/api/api-voip-webhook.md).

# API - VoIP Webhook

Public route used by external telephony providers/centrals to send already completed calls to SprintHub.

For each received event, SprintHub:

* identifies the lead by phone (or creates a new lead, if the number is unknown);
* logs the call in the call history (`voip_history`), visible in the CRM;
* logs the event in the lead timeline (`voip.call_incoming` or `voip.call_outgoing`).

<figure><img src="https://684109703-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=CLIENT_NAME
Content-Type: application/json
```

Replace `CLIENT_NAME` with the instance name (e.g.: `myclient`). The parameter `?i=` is what identifies the client's database and must always be present in the URL configured in the provider.

***

### Authentication

Authentication is **optional** and defined per instance:

| Situation                            | Behavior                                                                         |
| ------------------------------------ | -------------------------------------------------------------------------------- |
| Client **without** secret configured | The endpoint accepts the request without additional validation                   |
| Client **with** secret configured    | It is mandatory to send the secret, otherwise the response is `401 unauthorized` |

When the secret is active, send it in one of two ways:

```
x-voip-signature: YOUR_SECRET
```

or

```
POST /voip/webhook?i=CLIENT_NAME&secret=YOUR_SECRET
```

> The secret configuration (`voip_webhook_secret`) is done by the SprintHub team in the client's instance.

***

### Request body

The body is an envelope with two fields: `event` and `data`.

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

| Field   | Type   | Required | Description                         |
| ------- | ------ | -------- | ----------------------------------- |
| `event` | string | yes      | Only value accepted today: `"call"` |
| `data`  | object | yes      | Completed call data (table below)   |

#### Fields of `data`

| Field         | Type    | Required | Description                                                                 |
| ------------- | ------- | -------- | --------------------------------------------------------------------------- |
| `user`        | number  | **yes**  | SprintHub user ID who made/received the call. Must exist in the instance    |
| `leadPhone`   | string  | **yes**  | Contact phone number, with country code. Non-numeric characters are removed |
| `type`        | string  | **yes**  | `INCOMING` (received) or `OUTGOING` (made)                                  |
| `status`      | string  | **yes**  | `COMPLETE` (answered) or `INCOMPLETE` (not answered)                        |
| `duration`    | number  | **yes**  | Duration in **seconds**. Use `0` for unanswered calls                       |
| `record`      | boolean | **yes**  | Indicates whether the call was recorded in the central                      |
| `service`     | string  | **yes**  | Provider/central identifier (up to 32 characters). E.g.: `"twilio"`         |
| `lead`        | number  | no       | Lead ID, if already known. If omitted, the lead is resolved by phone        |
| `leadName`    | string  | no       | Contact name (up to 64 characters)                                          |
| `userName`    | string  | no       | Operator name (up to 64 characters)                                         |
| `callId`      | string  | no       | Call ID in the provider (up to 64 characters). Used for reconciliation      |
| `description` | string  | no       | Free-form note about the call (up to 1000 characters)                       |

> Fields not listed above are rejected: any extra property results in `400 invalid_body`.

#### Example — received and answered call

```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": "Client requested a commercial proposal"
  }
}
```

#### Example — made and unanswered call

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

#### Example with cURL

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

***

### Postman Collection

File: `voip_webhook_postman_collection.json` — import into Postman at *Import > File*.

Before running, adjust the collection variables:

| Variable    | Description                                                       |
| ----------- | ----------------------------------------------------------------- |
| `baseUrl`   | API URL (e.g.: `https://api.sprinthub.app`)                       |
| `instance`  | Instance name, used in the `?i=`                                  |
| `secret`    | Webhook secret. Leave empty if the instance does not use a secret |
| `userId`    | ID of an existing user in the instance                            |
| `leadPhone` | Phone number used in tests                                        |
| `service`   | Provider identifier sent in `service`                             |
| `leadId`    | Fill in only for the request *"with lead provided"*               |

The collection has two folders:

* **Success** — answered incoming call, unanswered outgoing call, minimal payload, with lead provided and with masked phone number. Each request validates `201`/`msg: created` and stores the returned ID in the variable `historyId`.
* **Errors** — the cases of `400` (missing required field, `event` invalid, invalid enum, extra field) and the `401` invalid secret.

> Requests in the folder **Success** create real records in the call history and may create the lead from the provided phone number. Use a test instance. The `callId` is generated with `{{$timestamp}}` so each submission is unique.

***

### What SprintHub does with the event

1. **Validates the body** — format, types and required fields.
2. **Normalizes the phone number** — removes everything that is not a digit from `leadPhone`.
3. **Resolves the lead**:
   * if `lead` was sent, it is used directly;
   * otherwise, it looks for an existing lead by the phone, mobile and WhatsApp fields (with handling of the 9th digit in Brazilian numbers);
   * if it does not find one, it creates a new lead with source **VoIP**, using the WhatsApp name and photo when the number has an active account, or the phone number itself as the name.
4. **Registers the call** in the call history and returns the generated ID.
5. **Registers in the lead timeline** the event `voip.call_incoming` or `voip.call_outgoing`, with operator, phone, status, duration, `callId` and `service`.

> If lead resolution/creation fails, the call is still recorded — just without a lead link (and, in that case, without a timeline event).

***

### Responses

| Code  | Body                                             | Meaning                                                              |
| ----- | ------------------------------------------------ | -------------------------------------------------------------------- |
| `201` | `{ "msg": "created", "id": 123 }`                | Call recorded. `id` is the identifier in the call history            |
| `400` | `{ "msg": "invalid_body", "err": { ... } }`      | Invalid body: missing required field, wrong type or extra field      |
| `401` | `{ "msg": "unauthorized" }`                      | Secret configured in the instance and not sent (or sent incorrectly) |
| `500` | `{ "msg": "failed_voip_webhook", "err": "..." }` | Failed to save the call. It can be resent                            |

The field `err` of the `400` brings the validation detail, useful for debugging the integration:

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

***

### Best practices

* **Send one event per completed call**, after the end — the webhook does not handle progress events (ringing, answered, etc.).
* **There is no deduplication by `callId`**: resending the same event creates a new record in the history. Always send `callId` to allow reconciliation and avoid unnecessary resends.
* **Resend only on error `500` or timeout**, with progressive wait between attempts. Since the failure may occur after the call has been recorded, a resend can generate a duplicate record — use the `callId` to identify.
* **`duration` in seconds**, always an integer.
* **`record: true` only indicates** that there is a recording in the central; the audio is not sent or attached by this endpoint.
* **Respect text field size limits** (`userName`, `leadName` and `callId` up to 64; `service` up to 32; `description` up to 1000). Larger values may be truncated or cause an error `500`.
* **Automation:** the trigger *"received call"* (`leadPhoneCall`, event `received`) recognizes the calls `INCOMING` received by this webhook. The filter by operator number does not apply, since the webhook does not send that number.
* This endpoint **does not generate credit usage or automatic transcription** — it only records the call history and the event on the lead.
