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

# Create a widget voice session

POST https://api.jelliu.co/widget/voice-session

Mints short-lived credentials for a browser voice conversation with the widget's agent: a signed
WebSocket URL, and when available a WebRTC conversation token (preferred, for echo cancellation and
noise removal; fall back to `signed_url` if WebRTC cannot connect). Only for widgets with
`voice_enabled: true`. No request body; call it per click and do not cache the result.

Credentials, origin check and limits are the same as `POST /widget/init`. It does not need a session.

**Side effects.** Calls the voice engine to mint the credentials for the agent's voice profile. Minting records
no usage by itself; the voice conversation that follows runs on the widget's agent.

**Idempotency.** Each call mints fresh credentials; retrying is harmless, and unused URLs expire.

**Access**
- **Required scope:** None; `jl_` API keys are not used. The widget credential plus a matching `x-widget-parent-origin` authorise the call.
- **Rate limit:** Widget public — 60 requests/min per IP, plus the widget's `rate_limit_rpm` per credential and 300 requests/min per workspace; the general limiter also counts 120 requests/min per IP. See [Rate limits](/rate-limits).
- **Plan:** Available on every plan.


Reference: https://developer.jelliu.co/api-reference/widgets/post-widget-voice-session

## Request

### Query parameters

- `wid` (string, optional) — Public widget id as a query parameter. Used when neither header is present.

### Headers

- `x-widget-api-key` (string, optional) — Secret widget API key (`plaintext_api_key` from `POST /api/widgets`). Takes precedence over `x-widget-id` and `wid`. Send one of the three. Never expose it in a browser.
- `x-widget-id` (string, optional) — Public widget id, the credential the embed script uses. Used when `x-widget-api-key` is absent.
- `x-widget-parent-origin` (string, required) — Origin of the page embedding the widget. Must match one of the widget's `allowed_origins`; see `POST /widget/init`.

## Response

### 200

Voice credentials.

- `data` (object, required)
  - `signed_url` (string, required) — Short-lived `wss://` URL that opens a voice conversation with the agent over WebSocket.
  - `conversation_token` (string, optional) — Short-lived WebRTC conversation token. Omitted when it could not be minted; use `signed_url` then.

## Errors

### 401 Unauthorized Error

`Missing widget credential` or `Invalid widget API key` (see `POST /widget/init`).

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 403 Forbidden Error

`Voice is not enabled for this widget`, or widget deactivated / workspace deleted / origin not allowed (see `POST /widget/init`).

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 409 Conflict Error

`Widget has no agent assigned` (`CONFLICT`), or `AGENT_PROVISIONING` when the agent (deleted or not yet created on the voice provider) has no voice profile; the latter carries `Retry-After: 5`.

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 429 Too Many Requests Error

A per-minute limit was hit (see `POST /widget/init` for the messages).

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 502 Bad Gateway Error

The voice engine refused to mint the signed URL (for example quota exhausted). Internals are not echoed.

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 503 Service Unavailable Error

The per-widget limiter's store is unreachable; it fails closed.

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

## Examples

**Response**

```json
{
  "data": {
    "signed_url": "wss://voice-engine.example/v1/conversation?agent_id=agent_01k2x9v7c3f8&conversation_signature=cvsig_7Hq2LmN4pR8sT1vW",
    "conversation_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZX19.4mQ9x"
  }
}
```

**SDK Code**

```python Widgets_postWidgetVoiceSession_example
import requests

url = "https://api.jelliu.co/widget/voice-session"

querystring = {"wid":"84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e"}

headers = {
    "x-widget-api-key": "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284",
    "x-widget-id": "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e",
    "x-widget-parent-origin": "https://www.clinicasonrisa.co"
}

response = requests.post(url, headers=headers, params=querystring)

print(response.json())
```

```javascript Widgets_postWidgetVoiceSession_example
const url = 'https://api.jelliu.co/widget/voice-session?wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e';
const options = {
  method: 'POST',
  headers: {
    'x-widget-api-key': '3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284',
    'x-widget-id': '84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e',
    'x-widget-parent-origin': 'https://www.clinicasonrisa.co'
  }
};

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

```go Widgets_postWidgetVoiceSession_example
package main

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

func main() {

	url := "https://api.jelliu.co/widget/voice-session?wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e"

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

	req.Header.Add("x-widget-api-key", "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284")
	req.Header.Add("x-widget-id", "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")
	req.Header.Add("x-widget-parent-origin", "https://www.clinicasonrisa.co")

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

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

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

}
```

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

url = URI("https://api.jelliu.co/widget/voice-session?wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")

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

request = Net::HTTP::Post.new(url)
request["x-widget-api-key"] = '3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284'
request["x-widget-id"] = '84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e'
request["x-widget-parent-origin"] = 'https://www.clinicasonrisa.co'

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

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

HttpResponse<String> response = Unirest.post("https://api.jelliu.co/widget/voice-session?wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")
  .header("x-widget-api-key", "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284")
  .header("x-widget-id", "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")
  .header("x-widget-parent-origin", "https://www.clinicasonrisa.co")
  .asString();
```

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

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.jelliu.co/widget/voice-session?wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e', [
  'headers' => [
    'x-widget-api-key' => '3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284',
    'x-widget-id' => '84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e',
    'x-widget-parent-origin' => 'https://www.clinicasonrisa.co',
  ],
]);

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

```csharp Widgets_postWidgetVoiceSession_example
using RestSharp;

var client = new RestClient("https://api.jelliu.co/widget/voice-session?wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e");
var request = new RestRequest(Method.POST);
request.AddHeader("x-widget-api-key", "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284");
request.AddHeader("x-widget-id", "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e");
request.AddHeader("x-widget-parent-origin", "https://www.clinicasonrisa.co");
IRestResponse response = client.Execute(request);
```

```swift Widgets_postWidgetVoiceSession_example
import Foundation

let headers = [
  "x-widget-api-key": "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284",
  "x-widget-id": "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e",
  "x-widget-parent-origin": "https://www.clinicasonrisa.co"
]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.jelliu.co/widget/voice-session?wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

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()
```