开发者 API

Callany API 参考

使用平台 API key 创建即时或定时通话任务,轮询同一个版本化任务契约,查看可用费率,在调度前取消,并通过鉴权路由下载已归档录音。

Bearer API key

在设置页创建密钥,然后从服务端代码以 Authorization: Bearer <API_KEY> 发送。完整密钥只展示一次。

幂等创建

重试创建电话时传入 idempotency-key header 或 idempotencyKey body 字段。该键按当前鉴权用户隔离。

私有产物

录音通过平台下载路由提供。不要在客户端保存或暴露上游 URL 或私有存储 key。

鉴权 Header
Authorization: Bearer <API_KEY>
幂等 Header
idempotency-key: <CLIENT_GENERATED_KEY>
JSON 错误 envelope
{ "code": -1, "message": "Unauthorized" }
示例

从服务端代码创建电话

把 API key 放在服务端环境变量里;所有创建重试路径都发送幂等键;定时请求发送本地墙上时间和明确的 IANA 时区。

curl

用 curl 创建即时电话

从 shell 环境读取 base URL 和 API key,并把客户端生成的幂等键放在请求 header 里。

curl
: "${AI_CALLS_API_KEY:?set AI_CALLS_API_KEY}"
: "${AI_CALLS_BASE_URL:?set AI_CALLS_BASE_URL}"

curl -sS -X POST "${AI_CALLS_BASE_URL%/}/api/v1/call-tasks" \
  -H "authorization: Bearer ${AI_CALLS_API_KEY}" \
  -H "content-type: application/json" \
  -H "idempotency-key: call-$(date -u +%Y%m%dT%H%M%SZ)-001" \
  --data '{
    "assistantId": "<ASSIGNED_ASSISTANT_ID>",
    "phoneNumberId": "<ASSIGNED_PHONE_NUMBER_ID>",
    "toNumber": "<E164_PHONE_NUMBER>",
    "toName": "Example Contact",
    "schedule": { "mode": "now" },
    "variables": {
      "customerName": "Example Contact",
      "reason": "appointment reminder"
    }
  }'
Node.js

用 Node.js 创建定时电话

使用 Node 环境变量,生成唯一幂等键,并发送本地墙上时间和 IANA 时区。

Node.js
import { randomUUID } from 'node:crypto';

const baseUrl = process.env.AI_CALLS_BASE_URL?.replace(/\/$/, '');
const apiKey = process.env.AI_CALLS_API_KEY;
if (!baseUrl || !apiKey) {
  throw new Error('Set AI_CALLS_BASE_URL and AI_CALLS_API_KEY');
}

const response = await fetch(baseUrl + '/api/v1/call-tasks', {
  method: 'POST',
  headers: {
    authorization: 'Bearer ' + apiKey,
    'content-type': 'application/json',
    'idempotency-key': 'scheduled-' + randomUUID(),
  },
  body: JSON.stringify({
    assistantId: '<ASSIGNED_ASSISTANT_ID>',
    phoneNumberId: '<ASSIGNED_PHONE_NUMBER_ID>',
    toNumber: '<E164_PHONE_NUMBER>',
    schedule: {
      mode: 'scheduled',
      localDateTime: '2026-07-01T07:30',
      timeZone: 'America/Los_Angeles',
    },
  }),
});

const envelope = await response.json();
if (!response.ok || envelope.code !== 0) {
  throw new Error(envelope.message || 'Callany API error');
}

console.log(envelope.data.id);
Python

用 Python 创建定时电话

API key 不写入源码;从服务端进程使用 requests;HTTP 状态之后继续检查平台 response envelope。

Python
import os
import uuid
import requests

base_url = os.environ["AI_CALLS_BASE_URL"].rstrip("/")
api_key = os.environ["AI_CALLS_API_KEY"]

payload = {
    "assistantId": "<ASSIGNED_ASSISTANT_ID>",
    "phoneNumberId": "<ASSIGNED_PHONE_NUMBER_ID>",
    "toNumber": "<E164_PHONE_NUMBER>",
    "schedule": {
        "mode": "scheduled",
        "localDateTime": "2026-07-01T07:30",
        "timeZone": "America/Los_Angeles",
    },
}

response = requests.post(
    base_url + "/api/v1/call-tasks",
    headers={
        "authorization": "Bearer " + api_key,
        "idempotency-key": "scheduled-" + str(uuid.uuid4()),
    },
    json=payload,
    timeout=30,
)
response.raise_for_status()

envelope = response.json()
if envelope.get("code") != 0:
    raise RuntimeError(envelope.get("message", "Callany API error"))

print(envelope["data"]["id"])
Node.js webhook

验证 Webhook 签名

解析 JSON 前先对请求原始 body 验证 HMAC,拒绝过期时间戳,并使用常量时间比较签名。每次重试保持事件 ID 和 body 不变,但会使用新的时间戳和签名。

Node.js webhook
import { createHmac, timingSafeEqual } from 'node:crypto';

const body = await request.text();
const timestamp = request.headers.get('x-ai-calls-timestamp') || '';
const received = (request.headers.get('x-ai-calls-signature') || '')
  .replace(/^v1=/, '');
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(ageSeconds) || ageSeconds > 300) {
  return new Response('stale signature', { status: 401 });
}

const expected = createHmac('sha256', process.env.AI_CALLS_WEBHOOK_SECRET)
  .update(timestamp + '.' + body)
  .digest();
const actual = Buffer.from(received, 'hex');
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
  return new Response('invalid signature', { status: 401 });
}

const event = JSON.parse(body);
return new Response(null, { status: 204 });
接口

当前公开通话 API

所有 JSON 接口返回平台 response envelope。录音下载在归档产物存在时返回二进制响应体。

创建即时电话

创建一条 AI 外呼任务,解析 E.164 目的地、预留积分并保存计费路由快照;返回 HTTP 202。

POST/api/v1/call-tasks
请求体
{
  "assistantId": "<ASSIGNED_ASSISTANT_ID>",
  "phoneNumberId": "<ASSIGNED_PHONE_NUMBER_ID>",
  "toNumber": "<E164_PHONE_NUMBER>",
  "toName": "Example Contact",
  "schedule": { "mode": "now" },
  "variables": {
    "customerName": "Example Contact",
    "reason": "appointment reminder"
  },
  "idempotencyKey": "call_2026_07_01_001"
}
响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "id": "<CALL_TASK_ID>",
    "reference": "<CALL_REFERENCE>",
    "mode": "immediate",
    "status": "queued",
    "cancelable": true,
    "destination": { "number": "<E164_PHONE_NUMBER>", "region": "US" },
    "schedule": { "dispatchAt": "2026-07-01T14:30:00.000Z", "timeZone": null },
    "charge": { "status": "reserved", "currency": "USD", "estimatedCents": 500, "heldCents": 500, "finalCents": 0 },
    "result": {
      "durationSeconds": null,
      "billableMinutes": null,
      "transcript": null,
      "summary": null,
      "recording": { "status": null, "available": false, "downloadUrl": null }
    },
    "error": null,
    "timestamps": {
      "createdAt": "2026-07-01T14:30:00.000Z",
      "startedAt": null,
      "endedAt": null,
      "completedAt": null
    }
  }
}

创建定时电话

创建一条未来外呼任务。发送 localDateTime 和明确的 IANA timeZone;Callany 只转换一次并保存 UTC 调度时刻。

POST/api/v1/call-tasks
请求体
{
  "assistantId": "<ASSIGNED_ASSISTANT_ID>",
  "phoneNumberId": "<ASSIGNED_PHONE_NUMBER_ID>",
  "toNumber": "<E164_PHONE_NUMBER>",
  "schedule": {
    "mode": "scheduled",
    "localDateTime": "2026-07-01T07:30",
    "timeZone": "America/Los_Angeles"
  },
  "idempotencyKey": "scheduled_call_2026_07_01_001"
}
响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "id": "<CALL_TASK_ID>",
    "reference": "<CALL_REFERENCE>",
    "mode": "scheduled",
    "status": "scheduled",
    "cancelable": true,
    "destination": { "number": "<E164_PHONE_NUMBER>", "region": "US" },
    "schedule": { "dispatchAt": "2026-07-01T14:30:00.000Z", "timeZone": "America/Los_Angeles" },
    "charge": { "status": "reserved", "currency": "USD", "estimatedCents": 500, "heldCents": 500, "finalCents": 0 },
    "result": {
      "durationSeconds": null,
      "billableMinutes": null,
      "transcript": null,
      "summary": null,
      "recording": { "status": null, "available": false, "downloadUrl": null }
    },
    "error": null,
    "timestamps": {
      "createdAt": "2026-07-01T13:00:00.000Z",
      "startedAt": null,
      "endedAt": null,
      "completedAt": null
    }
  }
}

列出通话任务

按 cursor 分页返回当前鉴权用户拥有的任务。可以按已公开的任务状态筛选,并用 nextCursor 继续查询。

GET/api/v1/call-tasks?limit=50&status=completed
响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "items": [{
      "id": "<CALL_TASK_ID>",
      "reference": "<CALL_REFERENCE>",
      "mode": "immediate",
      "status": "completed",
      "cancelable": false,
      "destination": { "number": "<E164_PHONE_NUMBER>", "region": "US" },
      "schedule": { "dispatchAt": "2026-07-01T14:30:00.000Z", "timeZone": null },
      "charge": { "status": "settled_provider_cost", "currency": "USD", "estimatedCents": 500, "heldCents": 0, "finalCents": 180 },
      "result": {
        "durationSeconds": 74,
        "billableMinutes": 2,
        "transcript": "...",
        "summary": "...",
        "recording": {
          "status": "archived",
          "available": true,
          "downloadUrl": "/api/v1/call-tasks/<CALL_TASK_ID>/recording"
        }
      },
      "error": null,
      "timestamps": {
        "createdAt": "2026-07-01T14:30:00.000Z",
        "startedAt": "2026-07-01T14:30:05.000Z",
        "endedAt": "2026-07-01T14:31:19.000Z",
        "completedAt": "2026-07-01T14:31:21.000Z"
      }
    }],
    "nextCursor": null
  }
}

获取通话任务详情

返回与创建和列表一致的平台任务契约,包括扣费、结果、产物可用性、转写和总结字段。

GET/api/v1/call-tasks/{id}
响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "id": "<CALL_TASK_ID>",
    "reference": "<CALL_REFERENCE>",
    "mode": "immediate",
    "status": "completed",
    "cancelable": false,
    "destination": { "number": "<E164_PHONE_NUMBER>", "region": "US" },
    "schedule": { "dispatchAt": "2026-07-01T14:30:00.000Z", "timeZone": null },
    "charge": { "status": "settled_provider_cost", "currency": "USD", "estimatedCents": 500, "heldCents": 0, "finalCents": 180 },
    "result": {
      "durationSeconds": 74,
      "billableMinutes": 2,
      "transcript": "...",
      "summary": "...",
      "recording": {
        "status": "archived",
        "available": true,
        "downloadUrl": "/api/v1/call-tasks/<CALL_TASK_ID>/recording"
      }
    },
    "error": null,
    "timestamps": {
      "createdAt": "2026-07-01T14:30:00.000Z",
      "startedAt": "2026-07-01T14:30:05.000Z",
      "endedAt": "2026-07-01T14:31:19.000Z",
      "completedAt": "2026-07-01T14:31:21.000Z"
    }
  }
}

取消通话任务

在调度开始前取消定时或排队中的任务。重复请求会返回同一个已取消任务,不会重复扣费或重复产生完成事件。

POST/api/v1/call-tasks/{id}/cancel
响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "id": "<CALL_TASK_ID>",
    "reference": "<CALL_REFERENCE>",
    "mode": "scheduled",
    "status": "canceled",
    "cancelable": false,
    "destination": { "number": "<E164_PHONE_NUMBER>", "region": "US" },
    "schedule": { "dispatchAt": "2026-07-01T14:30:00.000Z", "timeZone": "America/Los_Angeles" },
    "charge": { "status": "settled_canceled", "currency": "USD", "estimatedCents": 500, "heldCents": 0, "finalCents": 0 },
    "result": {
      "durationSeconds": null,
      "billableMinutes": null,
      "transcript": null,
      "summary": null,
      "recording": { "status": null, "available": false, "downloadUrl": null }
    },
    "error": null,
    "timestamps": {
      "createdAt": "2026-07-01T13:00:00.000Z",
      "startedAt": null,
      "endedAt": null,
      "completedAt": "2026-07-01T13:05:00.000Z"
    }
  }
}

解析目的地并报价

组合所选出站号码已确认的地区与目标 E.164 前缀,并返回权威计费路由和最大预留积分。

POST/api/v1/call-quotes
请求体
{
  "assistantId": "<ASSIGNED_ASSISTANT_ID>",
  "phoneNumberId": "<ASSIGNED_PHONE_NUMBER_ID>",
  "toNumber": "<E164_PHONE_NUMBER>",
  "destinationRegion": "US"
}
响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "origin": { "kind": "region", "region": "US" },
    "destination": {
      "schemaVersion": 2,
      "validity": "valid",
      "callingCode": "1",
      "resolution": { "kind": "exact", "region": "US" }
    },
    "pricing": {
      "routeCode": "NANP",
      "routeName": "North American Numbering Plan",
      "isFallback": false,
      "maximumReservationCents": 500
    }
  }
}

下载已归档录音

在产物归档后,为用户自己的任务下载录音二进制。该路由不会返回上游 URL 或存储 object key。

GET/api/v1/call-tasks/{id}/recording
响应
HTTP/1.1 200 OK
content-type: audio/mpeg
cache-control: private, no-store
x-recording-sha256: <SHA256>

<binary audio body>

创建 Webhook 端点

注册一个用户自有的 HTTPS 接收地址。签名密钥由平台生成且只在本次响应返回;离开页面或丢弃响应前必须保存在服务端。

POST/api/v1/webhook-endpoints
请求体
{
  "url": "https://example.com/webhooks/callany",
  "description": "Production receiver",
  "eventTypes": ["call.completed", "call.failed", "call.canceled"]
}
响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "id": "<WEBHOOK_ENDPOINT_ID>",
    "url": "https://example.com/webhooks/callany",
    "eventTypes": ["call.completed", "call.failed", "call.canceled"],
    "status": "pending",
    "verifiedAt": null,
    "secret": "<SAVE_THIS_ONCE>"
  }
}

验证 Webhook 端点

发送一条签名的 webhook.test 事件。只有接收端返回 2xx 后,端点才会变为 active;平台不会跟随重定向。

POST/api/v1/webhook-endpoints/{id}/test
响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "id": "<WEBHOOK_ENDPOINT_ID>",
    "url": "https://example.com/webhooks/callany",
    "eventTypes": ["call.completed", "call.failed", "call.canceled"],
    "status": "active",
    "verifiedAt": "2026-07-18T10:00:00.000Z"
  }
}

轮询 Webhook 投递状态

列出当前鉴权用户拥有的投递状态,可按 callTaskId、eventId、endpointId 或 status 过滤。接口不会返回密钥、事件原始载荷、上游数据或内部存储字段。

GET/api/v1/webhook-deliveries?callTaskId={id}
响应
{
  "code": 0,
  "message": "ok",
  "data": {
    "items": [{
      "id": "<WEBHOOK_DELIVERY_ID>",
      "event": {
        "id": "<EVENT_ID>",
        "type": "call.completed",
        "occurredAt": "2026-07-18T10:00:00.000Z"
      },
      "endpointId": "<WEBHOOK_ENDPOINT_ID>",
      "callTaskId": "<CALL_TASK_ID>",
      "status": "delivered",
      "attempts": 1,
      "nextAttemptAt": null,
      "lastHttpStatus": 204,
      "lastLatencyMs": 183,
      "deliveredAt": "2026-07-18T10:00:01.000Z"
    }]
  }
}