HCC Docs

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 the timezone you pass. A lead's zone comes first: timezone can't override it.
  • No zone at all. The call is placed, and the audit says its hours weren't checked. Pass timezone when 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 perRequests per 10 seconds
Live API key100
Test API key25
Workspace, across its keys300
IP address1,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.