> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oneshotagent.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Inbound Calls

> Let the agent's own phone number answer its calls

## Overview

An agent's phone number can **answer its own calls**. During business hours an assistant follows your prompt. After hours it takes a message, transfers the caller, or declines. Every call is transcribed, summarized, billed and receipted, and the agent is notified.

Setup takes three calls.

1. Get a number: `POST /v1/tools/voice/numbers`. It returns the agent's existing number for free, or buys one. An agent that has made a voice call already has one.
2. Configure it: `PUT /v1/tools/voice/numbers/{id}/inbound`.
3. Read the calls: `GET /v1/tools/voice/inbound`, or receive a signed webhook after each call.

## Authentication

Free endpoints, authenticated with the signed agent proof (`X-Agent-ID` + `x-agent-proof`), which the SDKs send for you. Changing the config needs a **wallet session** or an **OAuth session** (you, signed in through an app such as ChatGPT). Plain agent access tokens can read the config and calls but cannot change them, so a delegated credential cannot reroute your phone line.

## Getting a number

`POST /v1/tools/voice/numbers` (no body) gets the agent a voice number without placing a call.

* If the agent already has an active number, it is returned with `is_new: false` and nothing is charged (`200`).
* Otherwise a number is bought and the one-time phone registration fee is charged from **prepaid credits** (`201`, `is_new: true`, with a signed receipt: `category: communication`, `subcategory: voice_number`).
* The fee is taken before anything is bought. Too little credit returns `402` and buys nothing. A failed purchase is refunded.
* A second request while a number is being set up returns `409`. Retry it.
* Like changing the config, this needs a wallet or OAuth session.

## Billing

Inbound calls are paid from the agent's **prepaid credits** ([`POST /v1/credits/top-up`](/api-reference/credits/top-up)), per started minute at the voice call rate, with the voice call minimum. There is no quote step, because someone else placed the call.

* A call is **declined** when credits cannot cover the minimum call fee.
* A call never runs longer than the credits pay for, or than `max_minutes`.
* A declined call is free.
* Each billed call has a signed receipt (`category: communication`, `subcategory: voice_inbound`).

<Info>See [Pricing](/pricing) for the voice call rate.</Info>

## Request Body

<ParamField body="prompt" type="string" required>
  How the assistant answers: who it speaks for, what it can do, what it must not do. Max 8,000 characters.
</ParamField>

<ParamField body="first_message" type="string" optional>
  Greeting spoken when the call connects. Max 500 characters.
</ParamField>

<ParamField body="voice_id" type="string" optional>
  Voice id. Omit for the default voice.
</ParamField>

<ParamField body="language" type="string" optional>
  `en` (default), `es`, `fr`, `de`, `pt`, `it`, `nl`, or `multi`.
</ParamField>

<ParamField body="allowed_tools" type="string[]" optional>
  Call tools the assistant may use: `transfer_call`, `end_call`. Default `["end_call"]`.
</ParamField>

<ParamField body="transfer_number" type="string" optional>
  E.164 number to transfer to. Required with `transfer_call` or `after_hours: "transfer"`. Emergency numbers are refused.
</ParamField>

<ParamField body="business_hours" type="object" optional>
  `{ "timezone": "America/New_York", "windows": [{ "days": ["mon","tue","wed","thu","fri"], "start": "09:00", "end": "17:00" }] }`. `end` must be after `start`. Split an overnight shift into two windows. Omit (or `null`) for always open.
</ParamField>

<ParamField body="after_hours" type="string" optional>
  `take_message` (default): a short call that takes the caller's name, number and reason. `transfer`: forward to `transfer_number`. `decline`: speak `after_hours_message` and hang up.
</ParamField>

<ParamField body="after_hours_message" type="string" optional>
  What callers hear after hours. Max 500 characters.
</ParamField>

<ParamField body="max_minutes" type="number" optional>
  Longest call, 1–60. Default 10.
</ParamField>

<ParamField body="webhook_url" type="string" optional>
  HTTPS URL on a public host. After each call it receives a signed `voice.inbound.completed` event. The signing secret is returned once, when the URL is first set or changed.
</ParamField>

<ParamField body="enabled" type="boolean" optional>
  Default `true`. `false` saves the settings without answering calls.
</ParamField>

`PUT` replaces the whole config. Omitted optional fields revert to defaults, and omitting `webhook_url` removes the webhook.

## Example

<CodeGroup>
  ```typescript TypeScript theme={null}
  const number = await agent.provisionVoiceNumber(); // existing number, or buys one

  const inbound = await agent.setInboundVoice(number.id, {
    prompt: 'You answer for Acme Dental. Book cleanings and check-ups; take a message for anything else.',
    first_message: 'Hi, Acme Dental, how can I help?',
    business_hours: {
      timezone: 'America/New_York',
      windows: [{ days: ['mon', 'tue', 'wed', 'thu', 'fri'], start: '09:00', end: '17:00' }],
    },
    after_hours: 'take_message',
    webhook_url: 'https://example.com/hooks/calls',
  });
  // Store inbound.webhook_secret to verify webhook signatures.

  const { calls } = await agent.inboundCalls({ limit: 10 });
  ```

  ```python Python theme={null}
  number = client.provision_voice_number()  # existing number, or buys one
  client.set_inbound_voice(number["id"], {
      "prompt": "You answer for Acme Dental. Book cleanings and check-ups; take a message for anything else.",
      "business_hours": {
          "timezone": "America/New_York",
          "windows": [{"days": ["mon", "tue", "wed", "thu", "fri"], "start": "09:00", "end": "17:00"}],
      },
  })
  calls = client.inbound_calls(limit=10)["calls"]
  ```
</CodeGroup>

## Webhook

After each call, the `webhook_url` receives:

```json theme={null}
{
  "type": "voice.inbound.completed",
  "call": {
    "id": "3f1c…",
    "from": "+12125550199",
    "to": "+14155550123",
    "status": "ended",
    "ended_reason": "customer-ended-call",
    "duration_seconds": 75,
    "summary": "Caller booked a cleaning for Tuesday at 10am.",
    "transcript": "…",
    "charge_usdc": "…",
    "receipt_id": "rcpt_…"
  }
}
```

The `X-OneShot-Signature` header is `t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256 of `"<t>.<raw body>"` under your webhook secret. Reject events whose `t` is more than a few minutes old. Redirects are not followed, and delivery is best-effort. The call always appears in `GET /v1/tools/voice/inbound` and in the agent's notifications.

## Other endpoints

| Endpoint | Purpose |
| - | - |
| `POST /v1/tools/voice/numbers` | Get the agent a number (existing one free, or buy one) |
| `GET /v1/tools/voice/numbers` | The agent's numbers, with `inbound_enabled` |
| `GET /v1/tools/voice/numbers/{id}/inbound` | Current config |
| `DELETE /v1/tools/voice/numbers/{id}/inbound` | Stop answering (settings kept, disabled) |
| `GET /v1/tools/voice/inbound` | Calls, newest first: `limit` (≤100), `before`, `phone_number_id`, `include_transcript` |
| `GET /v1/tools/voice/inbound/{id}` | One call with transcript and recording URL |

<Note>Numbers idle for 30 days are released. Configuring a number and receiving calls both count as use.</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.