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

# Place Click-to-Call

> Ring the agent on your own number, then the customer, then bridge them

One request places a two-leg call: the **agent** is dialled first on your DID, and only when they answer is the **customer** dialled and the legs bridged. Both legs present your DID as the caller ID.

Requires a provisioned trunk (Gate T), a number assigned to that trunk, and a positive wallet balance. See the [Click-to-Call guide](/features/click-to-call) for the full flow.

<Warning>
  **A `reference_id` is permanent, and a failed call still consumes it.** Omit the field and one is minted per call, which is what you want unless you're deliberately making a request repeatable. To retry after a call that rang out, place a **new** call with a **new** reference.
</Warning>

## Request

### Headers

<ParamField header="Authorization" type="string" required>
  Bearer token with your API key
</ParamField>

### Body

<ParamField body="agent_number" type="string" required>
  The person dialled **first**. Indian mobile number as `+919812345678`, `919812345678` or `9812345678`. Spaces, hyphens, dots and brackets are stripped.
</ParamField>

<ParamField body="customer_number" type="string" required>
  Dialled once the agent answers. Must differ from `agent_number`.
</ParamField>

<ParamField body="did" type="string">
  The caller ID presented on **both** legs. Must be a number assigned to your workspace and to `outbound_trunk_id`. Defaults to your trunk's saved caller ID.
</ParamField>

<ParamField body="outbound_trunk_id" type="string">
  UUID of the outbound trunk to place the call over. Defaults to your workspace's active trunk.
</ParamField>

<ParamField body="reference_id" type="string">
  Your idempotency key, unique per trunk. 1–128 characters of `A-Za-z0-9._:-`. **Minted for you (`qc2c-<uuid>`) when omitted.**
</ParamField>

<ParamField body="callback_url" type="string">
  Public HTTPS URL that this call's events are relayed to. Requires `callback_secret`. Private, loopback, link-local and CGNAT addresses are rejected.
</ParamField>

<ParamField body="callback_secret" type="string">
  16–256 characters you choose. Required whenever `callback_url` is sent — it is the HMAC key your endpoint verifies `x-qcall-signature` against. Encrypted at rest and never returned; only its last four characters are readable afterwards.
</ParamField>

<ParamField body="agent_ring_timeout" type="integer" default="30">
  Seconds to ring the agent. `5`–`60`.
</ParamField>

<ParamField body="customer_ring_timeout" type="integer" default="45">
  Seconds to ring the customer. `5`–`60`.
</ParamField>

<ParamField body="max_duration" type="integer" default="3600">
  Maximum seconds of bridged talk time. `30`–`7200`.
</ParamField>

<ParamField body="record" type="boolean" default="false">
  Record the bridged conversation. Audio is uploaded **after** the call ends — poll the call with `?refresh=1`, then mint a playback URL.
</ParamField>

<ParamField body="metadata" type="object">
  Arbitrary JSON object echoed back on the call and in every webhook. At most 2048 bytes serialised, no NUL characters.
</ParamField>

## Response

**201 Created** — a call was admitted and the agent is being dialled.

**200 OK** — this `reference_id` was already bound to a live call. The original call is returned and **nothing new was dialled**.

The bodies are identical, so branch on `data.placed` / `data.replayed` rather than on the shape.

<ResponseField name="placed" type="boolean">
  `true` when a new call was dialled (201)
</ResponseField>

<ResponseField name="replayed" type="boolean">
  `true` when an existing call was returned and nothing was dialled (200)
</ResponseField>

<ResponseField name="call" type="object">
  <Expandable title="properties">
    <ResponseField name="call_id" type="string">Platform call id. Use it to poll, list events and mint recordings</ResponseField>
    <ResponseField name="reference_id" type="string">Your idempotency key, or the one minted for you</ResponseField>
    <ResponseField name="outbound_trunk_id" type="string">The trunk this call rides</ResponseField>
    <ResponseField name="did" type="string">Caller ID presented on both legs, `+E.164`</ResponseField>
    <ResponseField name="agent_number" type="string">`+E.164`</ResponseField>
    <ResponseField name="customer_number" type="string">`+E.164`</ResponseField>
    <ResponseField name="state" type="string">`pending`, `initiated`, `bridged`, `completed`, `failed`, `rejected` or `unknown`</ResponseField>
    <ResponseField name="call_done" type="boolean">`true` once the outcome is final</ResponseField>
    <ResponseField name="terminal_event" type="string | null">`call.completed`, `call.not_answered`, `call.failed` or `call.temporaryfailed`</ResponseField>
    <ResponseField name="hangup_cause" type="string | null">e.g. `NORMAL_CLEARING`, `NO_ANSWER`, `USER_BUSY`, `MAX_DURATION`</ResponseField>
    <ResponseField name="leg" type="string | null">On failures: `agent` or `customer` — which side failed</ResponseField>
    <ResponseField name="error" type="string | null">Machine-readable failure code, if any</ResponseField>
    <ResponseField name="error_detail" type="string | null">Human-readable detail for that failure</ResponseField>
    <ResponseField name="talk_seconds" type="integer | null">Bridged talk time. This is what gets billed</ResponseField>
    <ResponseField name="total_seconds" type="integer | null">Seconds from admission to the end of the call</ResponseField>
    <ResponseField name="start_time" type="string | null">ISO 8601</ResponseField>
    <ResponseField name="answer_time" type="string | null">ISO 8601. When the legs were bridged; `null` until then</ResponseField>
    <ResponseField name="end_time" type="string | null">ISO 8601. Present once the call has ended</ResponseField>
    <ResponseField name="metadata" type="object | null">Exactly what you sent</ResponseField>
    <ResponseField name="record_requested" type="boolean">Whether this call asked to be recorded</ResponseField>
    <ResponseField name="recording_id" type="string | null">Opaque id, once the platform has produced one</ResponseField>
    <ResponseField name="recording_status" type="string | null">e.g. `uploaded`</ResponseField>
    <ResponseField name="recording_available" type="boolean">`true` when a playback URL can be minted</ResponseField>
    <ResponseField name="callback_url" type="string | null">Where this call's events are relayed</ResponseField>
    <ResponseField name="callback_secret_last4" type="string | null">Last four characters of the stored signing secret — enough to tell which secret we hold, never enough to sign with</ResponseField>
    <ResponseField name="cost_usd" type="number | null">What this call cost, in USD</ResponseField>
    <ResponseField name="billing_status" type="string | null">`billed`, `failed`, or `null` when nothing was owed</ResponseField>
    <ResponseField name="placed_via" type="string">`api_key` or `session`</ResponseField>
    <ResponseField name="api_key_id" type="integer | null">Which API key placed the call</ResponseField>
    <ResponseField name="api_key_name" type="string | null">That key's name, snapshotted so a revoked key still explains its charges</ResponseField>
    <ResponseField name="created_by_user_id" type="integer | null">Who placed it</ResponseField>
    <ResponseField name="created_at" type="string">ISO 8601</ResponseField>
  </Expandable>
</ResponseField>

## Errors

| Status | Code                       | Meaning                                                                   |
| ------ | -------------------------- | ------------------------------------------------------------------------- |
| 400    | `INVALID_NUMBER`           | A number isn't a valid Indian mobile number                               |
| 400    | `SAME_NUMBER`              | `agent_number` and `customer_number` are the same                         |
| 400    | `INVALID_RING_TIMEOUT`     | Ring timeout outside 5–60 s                                               |
| 400    | `INVALID_MAX_DURATION`     | `max_duration` outside 30–7200 s                                          |
| 400    | `INVALID_RECORD`           | `record` isn't a boolean                                                  |
| 400    | `INVALID_METADATA`         | Not an object, over 2 KiB, or contains NUL                                |
| 400    | `INVALID_REFERENCE`        | `reference_id` fails the character/length rule                            |
| 400    | `INVALID_CALLBACK_URL`     | Not HTTPS, or not publicly resolvable                                     |
| 400    | `INVALID_CALLBACK_SECRET`  | Missing, or outside 16–256 characters                                     |
| 402    | `RECHARGE_REQUIRED`        | Wallet balance is `0` or less                                             |
| 403    | `CALLER_ID_NOT_OWNED`      | `did` isn't assigned to this workspace                                    |
| 403    | `destination_not_allowed`  | A number is outside your permitted prefixes                               |
| 404    | `REFERENCE_NOT_OWNED`      | That reference belongs to another workspace                               |
| 409    | `DIALER_NOT_PROVISIONED`   | Trunk not set up (Gate T)                                                 |
| 409    | `REFERENCE_ALREADY_USED`   | The reference is spent — place a new call                                 |
| 429    | `C2C_RATE_LIMITED`         | More than 20 calls placed in the last minute                              |
| 429    | `no_free_channel`          | Platform capacity exhausted. Honour `Retry-After`                         |
| 503    | `CARRIER_LINE_DOWN`        | Carrier line is down; your number and balance are fine                    |
| 503    | `C2C_NOT_CONFIGURED`       | Click-to-call isn't set up on the server                                  |
| 503    | `C2C_CALLBACK_UNREACHABLE` | The server's results address isn't reachable from the internet            |
| 504    | `C2C_OUTCOME_UNKNOWN`      | No answer from the platform — **check the agent's phone before retrying** |

Failures return `{ "success": false, "message": "...", "code": "..." }`, plus `request_id` when the refusal came from the call platform (**log it** — it's the handle support traces on) and `retry_after` in seconds on `429`/`503`.

<Note>
  `429` and `503` also carry a `Retry-After` header. A refused request never placed a call and never consumed its `reference_id`, so it is safe to retry with the same one.
</Note>

## Example

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.60db.ai/dialer/click2call \
    -H "Authorization: Bearer your-api-key" \
    -H "Content-Type: application/json" \
    -d '{
      "agent_number": "9812345678",
      "customer_number": "9876543210",
      "callback_url": "https://your-app.example.com/webhooks/qcall",
      "callback_secret": "a-long-random-string-you-choose",
      "agent_ring_timeout": 30,
      "customer_ring_timeout": 45,
      "max_duration": 3600,
      "metadata": { "lead_id": "L-991", "campaign": "q3-renewals" }
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch('https://api.60db.ai/dialer/click2call', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SIXTYDB_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      agent_number: '9812345678',
      customer_number: '9876543210',
      callback_url: 'https://your-app.example.com/webhooks/qcall',
      callback_secret: process.env.QCALL_WEBHOOK_SECRET,
      metadata: { lead_id: 'L-991' },
    }),
  });

  const { data } = await res.json();
  if (data.replayed) console.log('already placed:', data.call.call_id);
  else console.log('dialling:', data.call.call_id);
  ```

  ```python Python theme={null}
  import os, requests

  res = requests.post(
      "https://api.60db.ai/dialer/click2call",
      headers={"Authorization": f"Bearer {os.environ['SIXTYDB_API_KEY']}"},
      json={
          "agent_number": "9812345678",
          "customer_number": "9876543210",
          "callback_url": "https://your-app.example.com/webhooks/qcall",
          "callback_secret": os.environ["QCALL_WEBHOOK_SECRET"],
          "metadata": {"lead_id": "L-991"},
      },
  )

  data = res.json()["data"]
  print(data["call"]["call_id"], "placed" if data["placed"] else "replayed")
  ```
</RequestExample>

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "success": true,
    "data": {
      "placed": true,
      "replayed": false,
      "call": {
        "call_id": "7d004ba2-565f-4ee2-9aa2-19523d8db7ae",
        "reference_id": "qc2c-8f1c1e2a-4b77-4a51-9f3e-2c1d6f0a77b1",
        "outbound_trunk_id": "00000000-0000-4000-8000-000000000000",
        "did": "+917900000000",
        "agent_number": "+919812345678",
        "customer_number": "+919876543210",
        "state": "initiated",
        "call_done": false,
        "terminal_event": null,
        "hangup_cause": null,
        "leg": null,
        "error": null,
        "error_detail": null,
        "talk_seconds": null,
        "total_seconds": null,
        "start_time": null,
        "answer_time": null,
        "end_time": null,
        "metadata": { "lead_id": "L-991", "campaign": "q3-renewals" },
        "record_requested": false,
        "recording_id": null,
        "recording_status": null,
        "recording_available": false,
        "callback_url": "https://your-app.example.com/webhooks/qcall",
        "callback_secret_last4": "oose",
        "cost_usd": null,
        "billing_status": null,
        "placed_via": "api_key",
        "created_by_user_id": 4021,
        "api_key_id": 17,
        "api_key_name": "crm-integration",
        "created_at": "2026-09-23T14:11:27.134Z"
      }
    }
  }
  ```

  ```json 200 OK (replayed) theme={null}
  {
    "success": true,
    "data": {
      "placed": false,
      "replayed": true,
      "call": {
        "call_id": "7d004ba2-565f-4ee2-9aa2-19523d8db7ae",
        "reference_id": "crm-task-91823",
        "state": "initiated",
        "call_done": false
      }
    }
  }
  ```

  ```json 409 Reference spent theme={null}
  {
    "success": false,
    "message": "That reference has already been used for a call that has finished. A reference is permanent — place the call again to get a new one.",
    "code": "REFERENCE_ALREADY_USED"
  }
  ```
</ResponseExample>
