Calls from the SDK
import { Hcc, instructions as i } from '@hcc/sdk';
const hcc = new Hcc({ apiKey: process.env.HCC_API_KEY! });
const call = await hcc.calls.create({
from: '+14155550100', // one of your numbers
to: '+14155550123',
instructions: [
i.say('Hi, this is Acme calling about your appointment.'),
i.connectAgent('agt_123', { variables: { customerName: 'Ada' } }),
],
});
console.log(call.id, call.status); // methods resolve to the resource itself: 'call_…', 'queued'
// Later: change what the live call is doing, or end it.
await hcc.calls.update(call.id, { instructions: [i.say('Goodbye.'), i.hangup()] });
await hcc.calls.hangup(call.id);To hand the call straight to an AI agent, use the agentId shorthand (the same as instructions: [i.connectAgent(agentId, { variables })]):
await hcc.calls.create({
from: '+14155550100',
to: '+14155550123',
agentId: 'agt_123',
variables: { customerName: 'Ada' },
});These two need a live key. In test mode, agents join calls only through a flow, so a test key gets 400 agent_requires_flow.
What's checked before a call rings
Every call without a repId (instructions, agentId or flow) goes through the floor rules first, and each attempt is audited, placed or not:
- The Do Not Call list. 422
do_not_call, as on a rep's call. It's the list for your API key's environment. - Calling hours, 8:00 to 21:00 in the callee's time zone. 422
outside_calling_hours. The zone comes from the lead with this number, or else from thetimezoneyou pass. A lead's zone comes first:timezonecan't override it. - No zone at all. The call is placed, and the audit says its hours weren't checked. Pass
timezonewhen you know it.
await hcc.calls.create({
from: '+14155550100',
to: '+14155550123',
agentId: 'agt_123',
timezone: 'America/New_York', // used when no lead with this number has a time zone
});A test key runs the same checks, against the test list and test leads. A refusal is stored with its Idempotency-Key like any other answer, so if you pass your own key, use a new one to try again once the hours open.
Idempotency and retries
calls.create and webhookEndpoints.create always send an Idempotency-Key: one is generated when you don't pass { idempotencyKey }, and every retry of that call reuses it.
For 24 hours the API replays the first response to a key instead of running the request again (the replay carries the Idempotent-Replayed: true header), so a retried create never places two calls. Pass your own key to make retries across processes safe too:
await hcc.calls.create(params, { idempotencyKey: `reminder-${appointment.id}` });Reusing a key with a different request fails with 422 idempotency_key_reused; while the first request is still running the API answers 409 idempotency_key_in_use, which the SDK retries. Any other POST accepts { idempotencyKey } too, and is only retried when given one.
Reads, deletes and keyed POSTs are retried with exponential backoff on 429, 5xx and network errors (maxRetries, default 2). A Retry-After on the response is waited out in full instead, up to 60 seconds.
Rate limits
| Counted per | Requests per 10 seconds |
|---|---|
| Live API key | 100 |
| Test API key | 25 |
| Workspace, across its keys | 300 |
| IP address | 1,000 |
Past a limit, a request gets 429 rate_limited with Retry-After. It didn't run, it doesn't use up its Idempotency-Key, it isn't billed, and it isn't on the Logs page. The limits count per Cloudflare location and are approximate, so treat them as a ceiling to stay under, not a quota to fill.
The SDK waits out a 429 on anything it can safely retry. A POST with no Idempotency-Key isn't retried, so it throws HccApiError with code: 'rate_limited'. For bulk work, use leads.upsertMany or a file upload with leads.import rather than one request per lead.
Lists and pagination
Every list method takes { limit, cursor } (limit 1 to 100, default 20; newest first) and returns the same page type. Await it for one page, or for await it to walk every item across pages:
// Only calls in this key's environment: a test key never sees live calls.
const page = await hcc.calls.list({ limit: 50 }); // Page<Call>: { data, nextCursor, hasMore, nextPage() }
const next = await page.nextPage(); // null on the last page (nextCursor === null)
for await (const call of hcc.calls.list()) {
console.log(call.id, call.status); // fetches pages as it goes
}numbers, agents, integrations and webhookEndpoints follow the same pattern: list, plus get for numbers and agents and create/delete for webhook endpoints. numbers.release(id) unlinks a number of your connected telephony account (restoring its previous webhooks; it is never deleted there). integrations.list() lists your connected AI-agent platforms, and telephony.get() reports your telephony account (status, ownership, provider).
These aren't split by environment, so changes that reach production need a live key. A test key gets 403 live_mode_only on numbers.update, numbers.release, webhookEndpoints.create and webhookEndpoints.delete, and on reps.update with skills, enabled or defaultCallerId. It can still read them, and can still set a rep's state and device for simulated calls, except device on a phone verified with a real call.
Agents
Agents come from your connected AI-agent platforms; manage them there. Connect a platform in the dashboard and its agents are synced here, read-only (hcc.agents.list(), hcc.agents.get(id)). connectAgent takes our agt_… id or the platform's own agent id; when several of your integrations know the same id, pass provider:
i.connectAgent('agent_8f2k', { provider: 'acme-voice', variables: { plan: 'pro' } });
Errors
import { HccApiError } from '@hcc/sdk';
try {
await hcc.calls.get('call_missing');
} catch (error) {
if (error instanceof HccApiError) console.error(error.status, error.code, error.message);
}Network failures and timeouts throw HccConnectionError.