> This page is for version v2 (default).
> For other versions, use one of these documentation indexes:
> - v2 (default): https://docs.pinnacle.sh/v-2/llms.txt
> - v1: https://docs.pinnacle.sh/v1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pinnacle.sh/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pinnacle.sh/_mcp/server.

# Create Conference

POST https://api.pinnacle.sh/conferences
Content-Type: application/json

Create a conference: a room that calls can be added to. Each participant is an ordinary call.

You can add participants in the same request. Existing calls are added first, so the conference is live before dialed participants answer. With no participants the conference is `waiting` until the first one joins.

Send an `Idempotency-Key` to make dialed participants safe to retry: each one is dialed at most once.


Reference: https://docs.pinnacle.sh/api-reference/conferences/create-conference

## Authentication

- `PINNACLE-API-KEY` header (required) — API Key authentication via header

## Request

### Headers

- `Idempotency-Key` (string, optional)

### Body (application/json)

This endpoint expects a CreateConferenceParams.

- `label` (string, optional)
- `max_participants` (integer, optional, default: 10)
- `hold_audio_url` (string, optional)
- `speaking_events` (boolean, optional, default: false) — Send `participant.speaking` whenever the active speaker changes. Off by default because it can fire several times a second.
- `metadata` (map from string to string, optional)
- `participants` (list of AddParticipantParams, optional) — Participants to add right away. Existing calls are added first, so the conference is live before dialed participants answer.

## Response

### 201

Conference created.

- `id` (string, required)
- `label` (string, required, nullable)
- `status` (enum, required) — - `waiting`: Created, but nobody has joined yet. - `active`: At least one participant has joined. - `ended`: Over. Every participant's call has been hung up.
  - Allowed values: `waiting`, `active`, `ended`
- `participant_count` (integer, required) — Participants currently joined.
- `max_participants` (integer, required)
- `hold_audio_url` (string, required, nullable) — What participants on hold hear.
- `speaking_events` (boolean, required) — Whether `participant.speaking` events are sent when the active speaker changes.
- `metadata` (map from string to string, required)
- `created_at` (string, required)
- `started_at` (string, required, nullable)
- `ended_at` (string, required, nullable)
- `ended_reason` (enum, required, nullable) — Why a conference ended. A conference never ends just because one person is left. * `empty`: The last participant left. * `ended_by_api`: Ended with `DELETE /conferences/{id}` or the `conference.end` command. * `participant_ended_it`: A participant added with `end_conference_on_leave: true` left. * `time_limit`: Reached the 4-hour limit. * `expired_waiting`: Nobody joined in time. * `failed`: The conference couldn't be started.
  - Allowed values: `empty`, `ended_by_api`, `participant_ended_it`, `time_limit`, `expired_waiting`, `failed`
- `participants` (list of Participant, required)

## Errors

### 400 Bad Request Error

Validation failed. The payload has missing required fields and/or invalid types.&#x20; See [https://zod.dev/error-formatting](https://zod.dev/error-formatting) for more information.

- `description` (string, required) — Human-readable summary of validation failures.
- `errors` (ZodErrorErrors, required) — Structured dictionary of issues containing two main sections: - `errors`: Array of global validation errors not tied to specific fields - `properties`: Object mapping field names to their specific validation errors, where each field contains an `errors` array

### 401 Unauthorized Error

The request lacks valid authentication credentials or the provided credentials are invalid.&#x20; Ensure you're including a valid API key in the request headers and that your account has the necessary permissions to access this endpoint.

- `error` (string, required) — Human-readable description of the error that occurred, corresponding to the HTTP status code.

### 409 Conflict Error

This action cannot be completed right now because the resource is already being processed or is in a state that does not allow the requested change. Check the latest resource status and try again when it is ready.

- `error` (string, required) — Human-readable description of the error that occurred, corresponding to the HTTP status code.

### 500 Internal Server Error

An unexpected error occurred on Pinnacle's servers while processing your request.&#x20; If this error persists, please contact support with the request details.

- `error` (string, required) — Human-readable description of the error that occurred, corresponding to the HTTP status code.

### 502 Bad Gateway Error

One of Pinnacle's providers failed. Please try again later.

- `error` (string, required) — Human-readable description of the error that occurred, corresponding to the HTTP status code.

## Types

### AddParticipantParams

Either dial someone (`to` + `from`) or pull in an existing call (`call_id`). `muted` and `whisper_to` can't be combined.

### Participant

A call in a conference.

- `call_id` (string, required) — The participant is this call; every call API works on it.
- `conference_id` (string, required)
- `label` (string, required, nullable)
- `status` (enum, required) — - `dialing`: A dialed participant whose call hasn't answered yet. - `ringing`: Answered and being connected (or hearing its `announce`). - `joined`: In the conference. - `left`: Hung up or was removed. - `failed`: Never connected (busy, no answer, rejected, or an error).
  - Allowed values: `dialing`, `ringing`, `joined`, `left`, `failed`
- `from` (string, required, nullable)
- `to` (string, required, nullable)
- `muted` (boolean, required) — Nobody hears this participant; they still hear everyone.
- `on_hold` (boolean, required) — They hear hold audio and nobody hears them.
- `whisper_to` (list of string, required, nullable) — Call IDs of the only participants who can hear this one, or `null` for everyone.
- `end_conference_on_leave` (boolean, required) — When this participant leaves, the conference ends for everyone.
- `joined_at` (string, required, nullable)
- `left_at` (string, required, nullable)
- `left_reason` (enum, required, nullable)
  - Allowed values: `hangup`, `removed`, `busy`, `no_answer`, `rejected`, `failed`, `conference_ended`

### ZodErrorErrors

Structured dictionary of issues containing two main sections: - `errors`: Array of global validation errors not tied to specific fields - `properties`: Object mapping field names to their specific validation errors, where each field contains an `errors` array

### DialParticipantParams

Dial someone new. This creates a new outbound call, exactly like `POST /calls`.

- `to` (string, required) — E.164 number to dial.
- `from` (string, required) — One of your voice-enabled numbers.
- `label` (string, optional) — A name you choose for the participant, unique within the conference. Anywhere a participant is referenced you can use its label instead of its `call_id`. Can't start with `call_` or `leg_`.
- `muted` (boolean, optional)
- `on_hold` (boolean, optional)
- `whisper_to` (list of string, optional) — Only these participants (call IDs or labels) can hear this one; it still hears everyone. `null` means everyone hears it (the default). Stored as call IDs, so relabeling someone doesn't change who hears a whisper.
- `end_conference_on_leave` (boolean, optional)
- `announce` (Announce, optional) — Played to a dialed participant when they answer, before they join. Nobody else hears it. If it can't be played, they join without it.
- `ring_timeout_secs` (integer, optional)
- `record` (boolean, optional) — Same as `record` on `POST /calls`.
- `metadata` (map from string to string, optional) — String metadata carried on future call events. Maximum serialized size is 2 KB.

### ExistingCallParticipantParams

Pull in a call your team already has. It must be answered and not in another conference.

- `call_id` (string, required)
- `label` (string, optional) — A name you choose for the participant, unique within the conference. Anywhere a participant is referenced you can use its label instead of its `call_id`. Can't start with `call_` or `leg_`.
- `muted` (boolean, optional)
- `on_hold` (boolean, optional)
- `whisper_to` (list of string, optional) — Only these participants (call IDs or labels) can hear this one; it still hears everyone. `null` means everyone hears it (the default). Stored as call IDs, so relabeling someone doesn't change who hears a whisper.
- `end_conference_on_leave` (boolean, optional)

### Announce

Played to a dialed participant when they answer, before they join. Nobody else hears it. If it can't be played, they join without it.

### Speak

- `text` (string, required)
- `voice` (string, optional) — Text-to-speech voice. Defaults to `AWS.Polly.Joanna`.

### Play

- `url` (string, required) — Public `https` URL of an audio file.

## Examples

**Request**

```json
{
  "label": "escalation-4412",
  "participants": [
    {
      "call_id": "call_7cQe4F8xT1yB2mN9pK3L",
      "label": "caller"
    },
    {
      "to": "+14155551234",
      "from": "+14155559876",
      "label": "specialist"
    }
  ]
}
```

**Response**

```json
{
  "id": "conf_Hk2mX9pQ4rT7vB1nL8sW",
  "label": "escalation-4412",
  "status": "waiting",
  "participant_count": 1,
  "max_participants": 1,
  "hold_audio_url": "string",
  "speaking_events": true,
  "metadata": {},
  "created_at": "2024-01-15T09:30:00Z",
  "started_at": "2024-01-15T09:30:00Z",
  "ended_at": "2024-01-15T09:30:00Z",
  "ended_reason": "empty",
  "participants": [
    {
      "call_id": "call_7cQe4F8xT1yB2mN9pK3L",
      "conference_id": "conf_Hk2mX9pQ4rT7vB1nL8sW",
      "label": "supervisor",
      "status": "dialing",
      "from": "+14155559876",
      "to": "+14155551234",
      "muted": true,
      "on_hold": true,
      "whisper_to": [
        "string"
      ],
      "end_conference_on_leave": true,
      "joined_at": "2024-01-15T09:30:00Z",
      "left_at": "2024-01-15T09:30:00Z",
      "left_reason": "hangup"
    }
  ]
}
```

**SDK Code**

```python
import requests

url = "https://api.pinnacle.sh/conferences"

payload = {
    "label": "escalation-4412",
    "participants": [
        {
            "call_id": "call_7cQe4F8xT1yB2mN9pK3L",
            "label": "caller"
        },
        {
            "to": "+14155551234",
            "from": "+14155559876",
            "label": "specialist"
        }
    ]
}
headers = {
    "PINNACLE-API-KEY": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.pinnacle.sh/conferences';
const options = {
  method: 'POST',
  headers: {'PINNACLE-API-KEY': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"label":"escalation-4412","participants":[{"call_id":"call_7cQe4F8xT1yB2mN9pK3L","label":"caller"},{"to":"+14155551234","from":"+14155559876","label":"specialist"}]}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.pinnacle.sh/conferences"

	payload := strings.NewReader("{\n  \"label\": \"escalation-4412\",\n  \"participants\": [\n    {\n      \"call_id\": \"call_7cQe4F8xT1yB2mN9pK3L\",\n      \"label\": \"caller\"\n    },\n    {\n      \"to\": \"+14155551234\",\n      \"from\": \"+14155559876\",\n      \"label\": \"specialist\"\n    }\n  ]\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("PINNACLE-API-KEY", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.pinnacle.sh/conferences")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["PINNACLE-API-KEY"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"label\": \"escalation-4412\",\n  \"participants\": [\n    {\n      \"call_id\": \"call_7cQe4F8xT1yB2mN9pK3L\",\n      \"label\": \"caller\"\n    },\n    {\n      \"to\": \"+14155551234\",\n      \"from\": \"+14155559876\",\n      \"label\": \"specialist\"\n    }\n  ]\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.pinnacle.sh/conferences")
  .header("PINNACLE-API-KEY", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"label\": \"escalation-4412\",\n  \"participants\": [\n    {\n      \"call_id\": \"call_7cQe4F8xT1yB2mN9pK3L\",\n      \"label\": \"caller\"\n    },\n    {\n      \"to\": \"+14155551234\",\n      \"from\": \"+14155559876\",\n      \"label\": \"specialist\"\n    }\n  ]\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.pinnacle.sh/conferences', [
  'body' => '{
  "label": "escalation-4412",
  "participants": [
    {
      "call_id": "call_7cQe4F8xT1yB2mN9pK3L",
      "label": "caller"
    },
    {
      "to": "+14155551234",
      "from": "+14155559876",
      "label": "specialist"
    }
  ]
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'PINNACLE-API-KEY' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.pinnacle.sh/conferences");
var request = new RestRequest(Method.POST);
request.AddHeader("PINNACLE-API-KEY", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"label\": \"escalation-4412\",\n  \"participants\": [\n    {\n      \"call_id\": \"call_7cQe4F8xT1yB2mN9pK3L\",\n      \"label\": \"caller\"\n    },\n    {\n      \"to\": \"+14155551234\",\n      \"from\": \"+14155559876\",\n      \"label\": \"specialist\"\n    }\n  ]\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "PINNACLE-API-KEY": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [
  "label": "escalation-4412",
  "participants": [
    [
      "call_id": "call_7cQe4F8xT1yB2mN9pK3L",
      "label": "caller"
    ],
    [
      "to": "+14155551234",
      "from": "+14155559876",
      "label": "specialist"
    ]
  ]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.pinnacle.sh/conferences")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```