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

# Calling the API

> Base address, authentication, formats and the map of every request.

# Calling the API

## Calling the API

* **Base URL:** `https://{{HUBTALK_FQDN}}` — every path below starts with `/v1`. Substitute the host your own dashboard runs on for `{{HUBTALK_FQDN}}` — for example `app.hubtalk.ai`.
* **Format:** JSON in the request and in the response. Time values are **epoch seconds**.
* **Machine-readable schema:** `GET https://{{HUBTALK_FQDN}}/openapi.json`, with an interactive explorer at `https://{{HUBTALK_FQDN}}/docs`. The schema is authoritative for **requests**; success responses are published there as untyped objects, so the tables on this page are the response contract.

### Authentication

Every `/v1` request carries the key:

```
Authorization: Bearer YOUR_API_KEY
```

The secret (`cfk_` + hex) is issued in the dashboard and shown once ([The access key](/v4/web-api/start)); the platform stores only a SHA-256 hash. A revoked key stops working immediately — the key record is re-read on every request. A key is scoped to the organization it was issued in: anything outside that boundary is indistinguishable from missing (`404`), never `403`.

| Failure                                                    | Status | `error.type`           | `error.code`                                      |
| ---------------------------------------------------------- | ------ | ---------------------- | ------------------------------------------------- |
| missing or malformed `Authorization` header                | 401    | `authentication_error` | `api_key_required`                                |
| unknown key                                                | 401    | `authentication_error` | `api_key_unknown`                                 |
| revoked key                                                | 401    | `authentication_error` | `api_key_revoked`                                 |
| per-key rate limit exceeded                                | 429    | `rate_limit_error`     | `api_key_rate_limit_exceeded` (`Retry-After: 60`) |
| privileged-only operation with a regular key               | 403    | `permission_error`     | `privileged_key_required`                         |
| privileged operation with a key of a non-root organization | 403    | `permission_error`     | `root_org_key_required`                           |

None of the endpoints in this guide require a privileged key.

**Per-key rate limit.** Each key has a sliding 60-second window over all its `/v1` requests. The limit is set per key at creation and can be **changed later** in the dashboard without rotating the secret (it applies from the next request). A key limit of `0` means the platform default — a deployment setting, 60 requests per minute unless the operator changed it; an operator may also disable the limit. This is one of **four distinct 429 sources**.

### One error envelope

All `/v1` errors look the same:

```json theme={null}
{
  "error": {
    "type": "rate_limit_error",
    "code": "dial_queue_full",
    "status": 429,
    "message": "Dial queue is full; retry after the interval in Retry-After"
  }
}
```

| HTTP status | `error.type`                |
| ----------- | --------------------------- |
| 400, 422    | `invalid_request_error`     |
| 401         | `authentication_error`      |
| 403         | `permission_error`          |
| 404         | `not_found_error`           |
| 409         | `conflict_error`            |
| 429         | `rate_limit_error`          |
| 503         | `service_unavailable_error` |
| 500, 502    | `api_error`                 |

`error.code` names the exact cause within the type and is **always** present: when no more specific cause is registered, `code` equals `type`. New codes are added over time — handle an unrecognized code by falling back to `type` + `status`.

**Two additive fields** appear only on specific campaign refusals and nowhere else: `error.blockers` on `409 launch_blocked` and `error.import` on `422 calls_rejected` / `422 calls_dnc_matched`. Every other error carries exactly the four fields above.

<Warning>
  Classify the error on `error.type` + `error.status`, and the exact cause on `error.code`. **Never match on `error.message`** — it is a human-readable English diagnostic, looked up by code at the API boundary, and not part of the contract. On `422` schema-validation errors (`code: "invalid_request"`) `message` is an **array** of field-level issues in FastAPI/pydantic form (`{"type", "loc", "msg", "input"}`) rather than a string.
</Warning>

Refusals carry their headers through the envelope: `Retry-After` on 429s is always delivered.

### Rate limits and `429`

A `429` comes from four independent sources. All carry `error.type: "rate_limit_error"` — distinguish them by `error.code`:

| `error.code`                  | Source                                                                          | What to do                                                                               | `Retry-After`                |
| ----------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------- |
| `api_key_rate_limit_exceeded` | sliding 60-second window on all requests made with the key                      | slow down your overall request rate                                                      | yes, always `60`             |
| `dial_queue_full`             | the platform's dial queue is at its depth ceiling; **the call was not created** | repeat the same request with the **same** `Idempotency-Key` after the indicated interval | yes (queue estimate, 1–60 s) |
| `subgroup_concurrency_limit`  | the subgroup's concurrent-call limit is exhausted                               | wait for active calls to finish; no honest estimate exists, use your own backoff         | no (deliberate)              |
| `subgroup_rate_limit`         | the subgroup's per-minute initiation limit is exhausted                         | wait out the minute window                                                               | yes (`60`)                   |

Subgroup limits default to 10 concurrent calls and 30 initiations per minute unless the administrator configured other values; for a trunk used under a grant the effective limit is the smaller of the subgroup's and the grant's.

## Request map

**Campaigns**

| Method and path                       | Purpose                                                              |
| ------------------------------------- | -------------------------------------------------------------------- |
| `POST /v1/campaigns`                  | create the campaign, hand over the numbers, and launch — one request |
| `GET /v1/campaigns`                   | cursor-paginated list of campaigns (short views)                     |
| `GET /v1/campaigns/{id}`              | current state of one campaign                                        |
| `GET /v1/campaigns/{id}/launch-check` | `{blockers, warnings, dnc_screen}` before acting                     |
| `POST /v1/campaigns/{id}/launch`      | launch a campaign that stayed a draft                                |
| `POST /v1/campaigns/{id}/calls`       | add a batch of numbers to an existing campaign                       |
| `POST /v1/campaigns/{id}/pause`       | stop dialing new numbers                                             |
| `POST /v1/campaigns/{id}/resume`      | continue (launch checks run again)                                   |
| `POST /v1/campaigns/{id}/cancel`      | cancel irreversibly                                                  |
| `GET /v1/campaigns/{id}/progress`     | contact states, holds, pace, do-not-call and frequency counters      |

**Uploads**

| Method and path                                     | Purpose                                              |
| --------------------------------------------------- | ---------------------------------------------------- |
| `GET /v1/imports/{import_id}`                       | the upload summary plus the live or frozen screening |
| `GET /v1/imports/{import_id}/report?format=csv`     | per-row report, `csv` or `xlsx`                      |
| `GET /v1/imports/{import_id}/report?only=dnc_match` | only the do-not-call matches, `csv` or `xlsx`        |

**Lines**

| Method and path                     | Purpose                                               |
| ----------------------------------- | ----------------------------------------------------- |
| `GET /v1/dialing-routes?agent_id=…` | which trunks and subgroups are available to the agent |

**Individual calls**

| Method and path                 | Purpose                                     |
| ------------------------------- | ------------------------------------------- |
| `POST /v1/calls/phone`          | one outbound call                           |
| `POST /v1/calls/web`            | web call: a token for a browser or SDK      |
| `GET /v1/calls`                 | list of calls with filters and a cursor     |
| `GET /v1/calls/{call_id}`       | the full call object                        |
| `GET /v1/calls/{call_id}/turns` | per-turn latency metrics of a finished call |
| `POST /v1/calls/{call_id}/end`  | end a live call                             |
