Calls from the SDK
import { Hcc, instructions as i } from '@hccrun/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.
One 5xx is never retried: 504 call_outcome_unknown. The carrier didn't answer in time, so the call may be ringing anyway, and sending it again could call the person twice. Its key keeps that answer too. details.callId names the call: the carrier's updates still settle it, so read it with calls.get or wait for its events. Registering a rep's phone answers the same way, with details.verificationId, when its verification call may be ringing.
try {
await hcc.calls.create(params);
} catch (error) {
if (error instanceof HccApiError && error.code === 'call_outcome_unknown') {
const { callId } = error.details as { callId: string };
// Don't dial again: look at the call later.
}
}
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 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 '@hccrun/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.