> 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="/files/24ed6a0dbeb13e8ec4c9a13c692e77f3ae31e0d5" 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.sprinthub.com/en/topics/api/api-voip-webhook.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
