# API reference

> Every voicast API endpoint (/v1) and the voice connections: authentication, format, inputs, responses and errors.

Source: https://voicast.jp/en/docs/api

## Basics

- **URL**: `https://api.voicast.jp/v1`
- **Auth**: `Authorization: Bearer vk_live_…` or `vk_test_…`. A key reads and writes only inside its environment (live or test)
- **Format**: JSON. Times are Unix milliseconds; IDs are prefixed strings (`agt_`, `ten_`, `call_`, `sess_`, `con_`)
- **Errors**: `{ error, message, details?, request_id }`. See [Errors and limits](https://voicast.jp/en/docs/errors.md)
- **Request ID**: every response has `x-request-id` (`req_…`), the same as `request_id` in error bodies. Quote it when contacting support
- **Rate limit**: 600 requests per minute per API key. `x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset` (end of the window, Unix seconds) show what is left; over the limit you get 429 with `Retry-After` (seconds)

Create API keys under API keys in the dashboard (admin or above). Keep them on your server; never ship them to browsers or apps.

## Endpoints

| Method | Path | Description |
| --- | --- | --- |
| POST | [`/v1/agents`](#create-an-agent) | Create an agent |
| GET | [`/v1/agents`](#list-agents) | List agents |
| GET | [`/v1/agents/{id}`](#get-an-agent) | Get an agent |
| PUT | [`/v1/agents/{id}`](#update-an-agent) | Update an agent (new version) |
| GET | [`/v1/agents/{id}/versions`](#list-agent-versions) | List versions |
| DELETE | [`/v1/agents/{id}`](#delete-an-agent) | Delete an agent |
| POST | [`/v1/tenants`](#create-a-tenant) | Create a tenant |
| GET | [`/v1/tenants`](#list-tenants) | List tenants |
| GET | [`/v1/tenants/{id}`](#get-a-tenant) | Get a tenant |
| PATCH | [`/v1/tenants/{id}`](#update-a-tenant) | Update a tenant |
| POST | [`/v1/tenants/{id}/secret`](#rotate-a-tenant-secret) | Rotate the connection secret |
| DELETE | [`/v1/tenants/{id}`](#delete-a-tenant) | Delete a tenant |
| GET | [`/v1/calls`](#list-calls) | List calls |
| GET | [`/v1/calls/{id}`](#get-a-call) | Get a call |
| GET | [`/v1/calls/by-provider/{providerCallId}`](#get-a-call-by-provider-id) | Get a call by your provider's ID |
| GET | [`/v1/calls/{id}/transcript`](#get-a-transcript) | Get a transcript |
| POST | [`/v1/sessions`](#create-a-session) | Create a Web/app session |
| GET | [`/v1/usage`](#get-usage) | Usage breakdown |
| POST | [`/v1/webhooks/portal`](#create-a-webhook-portal-link) | Webhook portal link |
| GET | [`/v1/billing`](#get-billing) | Billing (balance, auto-recharge, per-minute price, payment method) |
| POST | [`/v1/billing/topups`](#create-a-top-up) | Top-up URL |
| GET | [`/v1/tools/secret`](#get-the-tools-signing-secret) | In-call tools signing secret |
| POST | [`/v1/tools/secret/rotate`](#rotate-the-tools-signing-secret) | Rotate the signing secret |
| GET | [`/v1/connectors`](#list-connectors) | List connectors |
| GET | [`/v1/connections`](#list-connections) | List connections |
| POST | [`/v1/connections`](#create-a-connection-api-key-or-url) | Create a connection (API key or URL) |
| GET | [`/v1/connections/{id}`](#get-a-connection) | Get a connection |
| PATCH | [`/v1/connections/{id}`](#configure-a-connection) | Configure a connection |
| GET | [`/v1/connections/{id}/options`](#connection-options) | Options for configuring a connection |
| DELETE | [`/v1/connections/{id}`](#delete-a-connection) | Delete a connection |
| GET | [`/v1/tenants/{id}/connections`](#list-a-tenants-connections) | List a tenant's connections |
| POST | [`/v1/tenants/{id}/connect-links`](#create-a-connect-page-link) | Connect-page link for a business |

Voice connections (no API key):

| Kind | URL | Description |
| --- | --- | --- |
| WebSocket | [`wss://voice.voicast.jp/ws`](#phone-twilio-media-streams) | Phone (Twilio Media Streams) |
| POST | [`https://voice.voicast.jp/v1/webrtc/offer`](#webrtc-offer) | Web and apps (WebRTC offer) |
| PATCH | [`https://voice.voicast.jp/v1/webrtc/offer`](#ice-candidates) | Add ICE candidates |

For MCP (`https://api.voicast.jp/mcp`), see [MCP](https://voicast.jp/en/docs/mcp.md).

---

## Create an agent

```
POST /v1/agents
```

| Input | Type | Description |
| --- | --- | --- |
| `name` | string | Name (1–100 chars) |
| `config` | AgentConfig | The agent config ([Agents](https://voicast.jp/en/docs/agents.md)) |

Returns 201 `Agent` (with `config`, `version: 1`). Invalid config returns 422 `invalid_config` with per-item `details`.

```ts
type Agent = { id: string; name: string; version: number; config?: AgentConfig; created_at: number; updated_at: number };

type AgentConfig = {
  greeting: string;            // first thing the AI says (up to 300 chars)
  persona: string;             // role and manner (up to 1,000 chars)
  fields: { key: string; label: string; required?: boolean; type?: 'text' | 'phone' }[]; // 1–20
  handoff_when: string[];      // handoff rules (up to 20)
  closing: string;             // closing when everything is collected (up to 300 chars)
  closing_answered?: string;   // closing when an FAQ answer settled the call
  faq?: { q: string; a: string }[];  // up to 50
  domain?: string;             // line of business (recognition hint)
  vocabulary?: string[];       // terms (recognition hints, up to 200)
  voice?: string;              // voice name (default Aoede)
  speaking_rate?: number;      // speaking rate (default 1.15)
  tools?: AgentTool[];         // tools (up to 10)
  connectors?: ConnectorEntry[]; // connections (up to 10)
};

type AgentTool = {
  name: string; description: string; url: string; confirm?: boolean;
  parameters?: { properties: Record<string, { type: 'string' | 'number' | 'integer' | 'boolean'; description?: string; enum?: string[] }>; required?: string[] };
};

type ConnectorEntry =
  | { connector: string; actions: string[]; confirm?: string[] }   // a preset
  | { connection: string; actions: string[]; confirm?: string[] }; // a specific connection (con_…)
```

## List agents

```
GET /v1/agents
```

Returns `{ data: Agent[] }` (without `config`).

## Get an agent

```
GET /v1/agents/{id}?version=
```

Omit `version` (an integer ≥ 1) for the current version. Returns `Agent` with `config`; 404 if not found.

## Update an agent

```
PUT /v1/agents/{id}
```

| Input | Type | Description |
| --- | --- | --- |
| `name` | string | Optional. Unchanged if omitted |
| `config` | AgentConfig | The whole config |

Returns `Agent` (the new version). Calls in progress keep the version they started with.

## List agent versions

```
GET /v1/agents/{id}/versions
```

Returns `{ data: [{ version, created_at }] }`, newest first.

## Delete an agent

```
DELETE /v1/agents/{id}
```

Returns 204. 409 `in_use` if a tenant uses it. Call records are kept.

## Create a tenant

```
POST /v1/tenants
```

| Input | Type | Description |
| --- | --- | --- |
| `external_id` | string | Your customer ID. 1–100 chars of letters, digits and `. _ : -`; unique within the environment |
| `name` | string | Optional. Display name (up to 100 chars) |
| `agent_id` | string | Optional. Default agent |

Returns 201 `Tenant` plus `secret` (the connection secret `vts_…`, returned only here). 409 `conflict` if the `external_id` exists.

```ts
type Tenant = { id: string; external_id: string; name: string; agent_id: string | null; status: 'active' | 'disabled'; created_at: number };
```

## List tenants

```
GET /v1/tenants
```

Returns `{ data: Tenant[] }`.

## Get a tenant

```
GET /v1/tenants/{id}
```

Returns `Tenant` (the connection secret is never returned here).

## Update a tenant

```
PATCH /v1/tenants/{id}
```

| Input | Type | Description |
| --- | --- | --- |
| `name` | string | Optional |
| `agent_id` | string \| null | Optional. `null` removes it |
| `status` | `active`, `disabled` | Optional. `disabled` stops phone and Web/app connections |

Returns `Tenant`. Only the fields you pass change.

## Rotate a tenant secret

```
POST /v1/tenants/{id}/secret
```

Returns `{ secret, previous_secret_expires_at }`. The old secret keeps working for 24 hours.

## Delete a tenant

```
DELETE /v1/tenants/{id}
```

Returns 204. The tenant can no longer connect and its connections are revoked. Call records are kept. You can create a tenant with the same `external_id` again.

## List calls

```
GET /v1/calls?tenant_id=&cursor=&limit=
```

| Query | Description |
| --- | --- |
| `tenant_id` | Optional. Filter by tenant |
| `cursor` | Optional. Where to continue (pass the previous response's `next_cursor` as is) |
| `limit` | Optional. 1–100 (default 50) |
| `before` | Optional. The older way to page (calls that started before this `started_at`). It can skip calls that started in the same millisecond, so use `cursor`. Cannot be combined with `cursor` (422) |

Returns `{ data: Call[], has_more, next_cursor }`, newest first. While `has_more` is `true`, pass `next_cursor` as `cursor` to read on (`next_cursor` is `null` when `has_more` is `false`).

```ts
type Call = {
  id: string; tenant_id: string; agent: { id: string; version: number };
  provider_call_id: string | null;   // Twilio CallSid, or the session ID for Web and apps
  from: string | null;               // caller's number (phone, when passed)
  started_at: number; ended_at: number | null; duration_ms: number | null;
  outcome: 'completed' | 'answered' | 'handoff' | 'hangup' | 'error' | null; reason: string | null;
  fields: Record<string, string> | null;
  summary: string | null; category: string | null; has_transcript: boolean;
  channel: 'phone' | 'web'; output: 'audio' | 'text';
  billed_yen: number | null;         // what this call costs you (yen before tax, up to 3 decimals); null while in progress
};
```

`billed_yen` is the amount taken from the balance for prepaid organizations (seconds × per-minute price ÷ 60), and seconds × the contract per-minute price ÷ 60 for invoiced organizations. Invoices round the month's total seconds up to whole minutes, so the sum of the calls can differ slightly ([Pricing and billing](https://voicast.jp/en/docs/pricing.md)).

## Get a call

```
GET /v1/calls/{id}
```

Returns `Call`.

## Get a call by provider ID

```
GET /v1/calls/by-provider/{providerCallId}
```

Look up a call by Twilio `CallSid` (`CA…`) or session ID (`sess_…`), for example from Twilio's `<Connect>` `action`. Returns `Call`; 404 if there is no record.

## Get a transcript

```
GET /v1/calls/{id}/transcript
```

Returns `{ call_id, turns: [{ role: 'assistant' | 'user', text }] }`. Created after the call ends; 404 if none.

## Create a session

```
POST /v1/sessions
```

| Input | Type | Description |
| --- | --- | --- |
| `tenant_id` | string | Tenant ID |
| `agent_id` | string | Optional. Defaults to the tenant's agent |
| `output` | `audio`, `text` | Optional. Default `audio` |

Returns 201 `{ id, tenant_id, agent_id, output, client_token, expires_at, webrtc_url, ice_servers }`. `client_token` (`vct_…`) lasts 2 minutes and works once. Pass `ice_servers` (`[{ urls, username?, credential? }]`) to `RTCPeerConnection`'s `iceServers` as is (STUN, plus short-lived TURN when available). Give only `client_token`, `webrtc_url` and `ice_servers` to the browser or app.

Errors: 402 (`balance_depleted`, `payment_overdue`, `spending_cap_reached`), 409 `tenant_disabled`, 422 `no_agent` or `invalid_request`.

## Get usage

```
GET /v1/usage?from=&to=&group_by=
```

| Query | Description |
| --- | --- |
| `from`, `to` | A date (`YYYY-MM-DD`, Japan time; `to` includes that day) or Unix milliseconds (`to` exclusive). Defaults to the start of this month until now. Up to 366 days |
| `group_by` | Optional. `tenant`, `day` or `channel` |

Returns:

```ts
type Usage = {
  environment: 'live' | 'test'; from: number; to: number;
  group_by: 'tenant' | 'day' | 'channel' | null; unit_price_yen: number;
  total: { calls: number; seconds: number; minutes: number; amount_yen: number };
  data: (({ tenant: { id: string; external_id: string; name: string } } | { day: string } | { channel: 'phone' | 'web' })
        & { calls: number; seconds: number; minutes: number; amount_yen: number })[];  // [] without group_by
  note: string;
};
```

Calls that ended within the period are counted. `amount_yen` is an estimate excluding tax ([Pricing and billing](https://voicast.jp/en/docs/pricing.md#usage-breakdown)).

## Create a webhook portal link

```
POST /v1/webhooks/portal
```

| Input | Type | Description |
| --- | --- | --- |
| `frame_origin` | string | Optional. Parent origin when embedding in an iframe |
| `locale` | `ja`, `en` | Optional. Page language (default `ja`) |

Returns 201 `{ url, expires_at }` (valid for 15 minutes): a page to register endpoint URLs, choose events and see signing secrets ([Results](https://voicast.jp/en/docs/results.md#webhooks)). 503 `unavailable` if webhook delivery is not available.

## Get billing

```
GET /v1/billing
```

Returns the billing state of the API key's organization (shared by the live and test environments), the same as Billing in the dashboard.

```ts
type Billing = {
  organization_id: string; month: string;            // this month (Japan time, YYYY-MM)
  contract: { payment_method: 'card' | 'invoice'; per_minute_yen: number; invoice_days_until_due: number };
  has_card: boolean;                                   // a card is saved for auto-recharge
  usage: { calls: number; seconds: number; minutes: number };  // this month's calls (live and test)
  estimate_yen: number;                                // prepaid: taken this month; invoice: the estimate
  balance: { yen: number; milli_yen: number; depleted: boolean; next_expiry: { yen: number; at: number } | null } | null;  // prepaid only
  auto_recharge: { enabled: boolean; threshold_yen: number; amount_yen: number; monthly_cap_yen: number | null;
                   recharged_this_month_yen: number; failures: number; max_failures: number } | null;  // prepaid only
  topup: { min_yen: number; max_yen: number; quick_yen: number[]; credit_months: number };
  spending_cap_yen: number | null; cap_reached: boolean;
  payment: { failed_at: number; grace_ends_at: number; suspended: boolean } | null;  // late invoice payment
  tax: 'excluded'; tax_rate: number;
  // also price, stripe_enabled, subscription_status, threshold
};
```

## Create a top-up

```
POST /v1/billing/topups
```

| Input | Type | Description |
| --- | --- | --- |
| `amount_yen` | integer | Amount in yen before tax, 500–300,000 |

Returns 201 `{ url, amount_yen, tax_yen }`. `url` is a Stripe payment page: open it in a browser and pay, and the amount is added to the balance. 422 outside the range, 409 `invoice_billing` for invoiced organizations. Auto-recharge, the monthly cap and the card are changed in the dashboard ([Pricing and billing](https://voicast.jp/en/docs/pricing.md)).

## Get the tools signing secret

```
GET /v1/tools/secret
```

Returns `{ secret, previous_secret_expires_at }`. `secret` (`whsec_…`) signs the environment's [in-call tool](https://voicast.jp/en/docs/tools.md) requests. `previous_secret_expires_at` is when the previous secret stops being used after a rotation (`null` if none).

## Rotate the tools signing secret

```
POST /v1/tools/secret/rotate
```

Returns `{ secret, previous_secret_expires_at }`. For 24 hours after a rotation, requests are signed with both the new and the previous secret; switch your server to the new one within that time.

## List connectors

```
GET /v1/connectors
```

Returns `{ data: Connector[] }`.

```ts
type Connector = {
  id: string; name: string; description: string;
  category: 'scheduling' | 'crm' | 'notify' | 'custom'; auth: 'oauth2' | 'api_key' | 'webhook_url';
  available: boolean;      // usable on this voicast
  needs_setup: boolean;    // needs configuration after connecting
  notify: boolean;         // posts a notification after each call
  env_only: boolean;       // environment connections only (not on the embeddable connect page)
  inputs: { name: string; type: 'text' | 'password' | 'url' | 'number' }[];
  actions: { name: string; tool: string; description: string; confirm: boolean }[]; // tool is the name shown to the AI
};
```

## List connections

```
GET /v1/connections?tenant_id=
```

Without `tenant_id`: all connections. With an empty value (`?tenant_id=`): environment connections only. With an ID: that tenant's connections. Deleted connections are not listed. Returns `{ data: Connection[] }`.

```ts
type Connection = {
  id: string; connector: string; tenant_id: string | null;
  label: string;           // the provider account (clinic@example.com, workspace #channel and so on)
  status: 'active' | 'needs_setup' | 'needs_reauth';
  config: object; created_by: string | null; created_at: number; updated_at: number;
};
```

## Create a connection (API key or URL)

```
POST /v1/connections
```

| Input | Type | Description |
| --- | --- | --- |
| `connector` | `teams`, `shannon_booking`, `http` | Connectors that take an API key or URL |
| `secret` | string | The API key or webhook URL (1–2,000 chars) |
| `input` | object | Optional. `{ base_url }` for `shannon_booking`, `{ base_url, header }` for `http` |
| `tenant_id` | string | Optional. Makes it a tenant connection. `shannon_booking` and `http` are environment-only: passing it returns 422 `invalid_request` |

Returns 201 `Connection`. voicast makes one test call before saving; failure returns 422 `verify_failed`. OAuth connectors are connected through a [connect-page link](#create-a-connect-page-link) or the dashboard.

## Get a connection

```
GET /v1/connections/{id}
```

Returns `Connection`.

## Configure a connection

```
PATCH /v1/connections/{id}
```

| Input | Type | Description |
| --- | --- | --- |
| `config` | object | Connector-specific settings. Only the fields you pass change |

Returns `Connection`. A `needs_setup` connection becomes `active`. Invalid settings return 422 `invalid_config`. See [Connections](https://voicast.jp/en/docs/connectors.md) for each connector's settings.

## Connection options

```
GET /v1/connections/{id}/options
```

Asks the provider for the choices available when configuring.

| Connector | Query | Returns |
| --- | --- | --- |
| Google Calendar, Microsoft 365 | | `{ calendars: [{ id, name, primary }] }` |
| Google Sheets | `spreadsheet`, `sheet` | `{ spreadsheets, sheets, headers }` |
| kintone | `app` | `{ apps, fields: [{ code, label }] }` |
| Chatwork | | `{ rooms }` |
| LINE WORKS | | `{ users }` |
| MCP server | | `{ tools }` |

409 `needs_reauth` if the connection is broken.

## Delete a connection

```
DELETE /v1/connections/{id}
```

Returns 204 and revokes the provider's token.

## List a tenant's connections

```
GET /v1/tenants/{id}/connections
```

Returns `{ data: Connection[] }`.

## Create a connect-page link

```
POST /v1/tenants/{id}/connect-links
```

| Input | Type | Description |
| --- | --- | --- |
| `connectors` | string[] | Optional. Connectors to show (`google_calendar`, `m365_calendar`, `google_sheets`, `kintone`, `hubspot`, `slack`, `chatwork`, `lineworks`, `teams`) |
| `frame_origin` | string | Optional. https parent origin when embedding in an iframe |

Returns 201 `{ url, expires_at }` (valid for 15 minutes). See [Connections](https://voicast.jp/en/docs/connectors.md#embeddable-connect-page).

---

## Phone (Twilio Media Streams)

```
wss://voice.voicast.jp/ws
```

The target of Twilio's `<Connect><Stream>`. Pass these as `<Parameter>`s:

| Parameter | Description |
| --- | --- |
| `tenant` | Tenant ID |
| `secret` | The tenant's connection secret (`vts_…`) |
| `agent` | Optional. Agent ID |
| `from` | Optional. The caller's number |

The stream is closed immediately when the secret is wrong, the tenant is disabled or calls are refused for billing reasons. See [Phone](https://voicast.jp/en/docs/phone.md).

## WebRTC offer

```
POST https://voice.voicast.jp/v1/webrtc/offer
```

| Input | Type | Description |
| --- | --- | --- |
| `sdp`, `type` | string | The WebRTC offer |
| `token` | string | The session's `client_token` (Pipecat clients may send it as `requestData.token`) |
| `pc_id`, `restart_pc` | string, boolean | Renegotiation only (no `token` needed) |

Returns `{ sdp, type, pc_id }`. 403 when the token is wrong, expired or used, or when calls are refused for billing reasons (billing refusals will later return 402, like `POST /v1/sessions`).

Once connected, send RTVI `client-ready` on the data channel, then a `ping` string every second ([Web and apps](https://voicast.jp/en/docs/web.md)).

## ICE candidates

```
PATCH https://voice.voicast.jp/v1/webrtc/offer
```

| Input | Type | Description |
| --- | --- | --- |
| `pc_id` | string | `pc_id` from the offer response |
| `candidates` | array | `[{ candidate, sdp_mid, sdp_mline_index }]` |

Returns `{ ok: true }`. 404 for an unknown `pc_id`.
