VICIfast API SDKs for TypeScript and Python
Install @vicifast/api or vicifast, make your first calls, page through lists, handle errors and retries safely, verify webhooks and test in the sandbox - with every SDK method listed.
The VICIfast API has two official SDKs: @vicifast/api for TypeScript and Node.js, and vicifast for Python. They call the same REST API documented at /api-docs, and they are generated from the same OpenAPI document, so every endpoint is a method and every method takes exactly what the endpoint takes. Neither package has dependencies.
You do not need an SDK to use the API - every request is plain HTTPS and JSON, and the reference shows each one in curl, Node.js, Python and PHP. What the SDKs add is the part that is easy to get subtly wrong: a safe Idempotency-Key on every call that spends money, retries only where retrying is safe, pages, typed errors and webhook signature checks.
How do I install an SDK?
npm install @vicifast/api # Node.js 18.17 or later
pip install vicifast # Python 3.8 or later
Then create an API key in the dashboard under API → Keys. A key that starts vf_test_ works against the sandbox: the same calls and the same answers, with a pretend $100 wallet and nothing real bought or charged. Build with a test key, then swap in a live one (vf_live_…) - nothing else changes.
Your first calls
The client takes the key; everything else has a sensible default.
import { Vicifast, VicifastError, parseEvent } from '@vicifast/api';
const vf = new Vicifast({ apiKey: process.env.VICIFAST_KEY! });
const { data: balance } = await vf.getBalance();
console.log(`wallet: ${balance.balance_cents} cents, live: ${vf.livemode}`);
const found = await vf.searchNumbers({ query: { area_code: '305', limit: 5 } });
const first = found.data[0]!.number;
const order = await vf.purchaseNumbers({ body: { numbers: [first] } });
// The answer is a quote when you send dry_run: true, a purchase otherwise.
if (order.data.object === 'purchase') console.log(order.data.order_id);
import os
from vicifast import Vicifast, VicifastError, parse_event
vf = Vicifast(api_key=os.environ["VICIFAST_KEY"])
balance = vf.get_balance()["data"]
print(f"wallet: {balance['balance_cents']} cents, live: {vf.livemode}")
found = vf.search_numbers(area_code="305", limit=5)
first = found["data"][0]["number"]
order = vf.purchase_numbers({"numbers": [first]})
# The answer is a quote when you send "dry_run": True, a purchase otherwise.
if order["data"]["object"] == "purchase":
print(order["data"]["order_id"])
How are the methods named?
Each endpoint's method is its operationId from the reference: getBalance, purchaseNumbers, listCalls. Python uses the same name in snake_case: get_balance, purchase_numbers, list_calls.
- TypeScript takes one object:
pathfor the parts of the URL,queryfor the query string,bodyfor the JSON body. The types say which of the three each method needs, and every response is typed. - Python takes the path parameters first, in order, then the body, then query parameters as keyword arguments:
vf.update_number("+13055550101", {"auto_renew": False}). A query parameter whose name is a Python keyword takes a trailing underscore:vf.list_calls(from_="+13055550101").
await vf.updateNumber({ path: { number: '+13055550101' }, body: { auto_renew: false } });
const today = await vf.listCalls({ query: { from: '+13055550101', window: '24h' } });
vf.update_number("+13055550101", {"auto_renew": False})
today = vf.list_calls(from_="+13055550101", window="24h")
The full list is at the end of this guide.
How do I get every page of a list?
A list answers one page at a time with has_more and next_cursor. paginate follows the cursor for you and hands back one item at a time, fetching the next page only when you reach it.
for await (const number of vf.paginate('listNumbers', { query: { status: 'active' } })) {
console.log(number.number, number.routing);
}
// The calls export is CSV, one file per page of up to 10,000 calls.
for await (const csv of vf.exportCallFiles({ query: { window: '30d' } })) {
process.stdout.write(csv);
}
for number in vf.paginate("listNumbers", status="active"):
print(number["number"], number["routing"])
# The calls export is CSV, one file per page of up to 10,000 calls.
for csv in vf.export_call_files(window="30d"):
print(csv, end="")
What happens when a call fails?
Anything other than a success raises a VicifastError. Branch on its code, never on the message: the codes are listed in the reference. It also carries the HTTP status, the offending param where there is one, any details, and a request id to quote to support.
try {
await vf.purchaseNumbers({ body: { numbers: ['+13055550199'] } });
} catch (e) {
if (e instanceof VicifastError && e.code === 'insufficient_funds') {
console.log(`top up first (request ${e.requestId})`);
} else {
throw e;
}
}
try:
vf.purchase_numbers({"numbers": ["+13055550199"]})
except VicifastError as e:
if e.code == "insufficient_funds":
print(f"top up first (request {e.request_id})")
else:
raise
Are retries safe?
Yes, and that is most of the reason to use an SDK. A request is retried - twice by default, on a timeout, a dropped connection, a 429 or a 5xx - only when repeating it cannot do anything twice:
- Reads are always safe to repeat.
- Calls that spend or move money - buying, releasing, back-ordering, transcribing - are sent with an
Idempotency-Keythe SDK makes for you, and the same key goes with every retry. The API answers a repeated key with the original result, so a retried purchase still buys once. - Everything else is sent once, and a failure is yours to decide about.
A 429 waits as long as its Retry-After header asks. To make a purchase exactly-once across a restart of your own program, choose the key yourself (idempotencyKey in TypeScript, idempotency_key= in Python) and store it with the order before you send it.
How do I check a webhook is from VICIfast?
Every webhook carries an X-VICIfast-Signature header signed with your endpoint's secret. Check it against the body exactly as it arrived - before any JSON parsing - and refuse anything that fails. parseEvent checks the signature and the timestamp (a request signed more than five minutes ago is refused, so a captured one cannot be replayed) and returns the event; in TypeScript the event is typed by its type.
export function onWebhook(rawBody: Buffer, signature: string | undefined): void {
const event = parseEvent(rawBody, signature, process.env.VICIFAST_WEBHOOK_SECRET!);
if (event.type === 'call.inbound.completed') {
console.log('a call ended', event.data);
}
}
def on_webhook(raw_body: bytes, signature: str) -> None:
event = parse_event(raw_body, signature, os.environ["VICIFAST_WEBHOOK_SECRET"])
if event["type"] == "call.inbound.completed":
print("a call ended", event["data"])
Wire it to your framework's raw request body - express.raw() in Express, request.get_data() in Flask. Receiving VICIfast webhooks covers endpoints, events and retries.
Testing in the sandbox
With a test key, the sandbox can make things happen on demand, so you can watch your code react without waiting for a real call or a real month. Each simulation sends the same webhooks a real one would.
await vf.simulateInboundCall({ body: { number: first, duration_s: 42 } });
await vf.timeTravel({ body: { days: 31 } }); // renewals fall due
await vf.setSandboxWallet({ body: { balance_cents: 150 } }); // a low wallet
await vf.resetSandbox();
vf.simulate_inbound_call({"number": first, "duration_s": 42})
vf.time_travel({"days": 31}) # renewals fall due
vf.set_sandbox_wallet({"balance_cents": 150}) # a low wallet
vf.reset_sandbox()
A live key refuses these with sandbox_only. Testing in the sandbox has the rest.
Every method
Every endpoint, with its method in each SDK. Optional parts are shown as the API takes them; the reference has every field.
Numbers
| What it does | Method |
|---|---|
| List your numbers GET /api/v1/numbers | TS vf.listNumbers({ query })Py vf.list_numbers(**query) |
| Change many numbers at once PATCH /api/v1/numbers | TS vf.bulkUpdateNumbers({ body })Py vf.bulk_update_numbers(body) |
| Search numbers for sale GET /api/v1/numbers/available | TS vf.searchNumbers({ query })Py vf.search_numbers(**query) |
| Buy numbers POST /api/v1/numbers/purchase | TS vf.purchaseNumbers({ body })Py vf.purchase_numbers(body) |
| Give numbers back POST /api/v1/numbers/release | TS vf.releaseNumbers({ body })Py vf.release_numbers(body) |
| Get a number GET /api/v1/numbers/{number} | TS vf.getNumber({ path: { number } })Py vf.get_number(number) |
| Update a number PATCH /api/v1/numbers/{number} | TS vf.updateNumber({ path: { number }, body })Py vf.update_number(number, body) |
| List your servers GET /api/v1/servers | TS vf.listServers()Py vf.list_servers() |
| List a server's CID groups GET /api/v1/servers/{id}/cid-groups | TS vf.listCidGroups({ path: { id } })Py vf.list_cid_groups(id) |
| Get a CID group, with its numbers GET /api/v1/servers/{id}/cid-groups/{group} | TS vf.getCidGroup({ path: { id, group } })Py vf.get_cid_group(id, group) |
| Put numbers into a CID group POST /api/v1/servers/{id}/cid-groups/{group}/numbers | TS vf.addCidGroupNumbers({ path: { id, group }, body })Py vf.add_cid_group_numbers(id, group, body) |
| Take numbers out of a CID group POST /api/v1/servers/{id}/cid-groups/{group}/numbers/remove | TS vf.removeCidGroupNumbers({ path: { id, group }, body })Py vf.remove_cid_group_numbers(id, group, body) |
Orders
| What it does | Method |
|---|---|
| List orders GET /api/v1/orders | TS vf.listOrders({ query })Py vf.list_orders(**query) |
| Get an order, with its numbers GET /api/v1/orders/{id} | TS vf.getOrder({ path: { id } })Py vf.get_order(id) |
| List back-orders GET /api/v1/backorders | TS vf.listBackorders({ query })Py vf.list_backorders(**query) |
| Place a back-order POST /api/v1/backorders | TS vf.placeBackorder({ body })Py vf.place_backorder(body) |
| Get a back-order GET /api/v1/backorders/{id} | TS vf.getBackorder({ path: { id } })Py vf.get_backorder(id) |
| Cancel a back-order POST /api/v1/backorders/{id}/cancel | TS vf.cancelBackorder({ path: { id } })Py vf.cancel_backorder(id) |
Calls
| What it does | Method |
|---|---|
| List calls GET /api/v1/calls | TS vf.listCalls({ query })Py vf.list_calls(**query) |
| Get a call GET /api/v1/calls/{id} | TS vf.getCall({ path: { id } })Py vf.get_call(id) |
| Export calls as CSV GET /api/v1/calls/export | TS vf.exportCalls({ query })Py vf.export_calls(**query) |
| Get a call's transcript GET /api/v1/calls/{id}/transcript | TS vf.getTranscript({ path: { id } })Py vf.get_transcript(id) |
| Transcribe a call POST /api/v1/calls/{id}/transcript | TS vf.createTranscript({ path: { id }, body })Py vf.create_transcript(id, body=None) |
| Get a recording link GET /api/v1/calls/{id}/recording | TS vf.getRecordingLink({ path: { id } })Py vf.get_recording_link(id) |
Billing
| What it does | Method |
|---|---|
| Get the wallet balance GET /api/v1/billing/balance | TS vf.getBalance()Py vf.get_balance() |
| List wallet transactions GET /api/v1/billing/transactions | TS vf.listTransactions({ query })Py vf.list_transactions(**query) |
| List upcoming renewals GET /api/v1/billing/renewals | TS vf.listRenewals({ query })Py vf.list_renewals(**query) |
Messaging
| What it does | Method |
|---|---|
| Send a message POST /api/v1/messages | TS vf.sendMessage({ body })Py vf.send_message(body) |
| List messages GET /api/v1/messages | TS vf.listMessages({ query })Py vf.list_messages(**query) |
Journeys
| What it does | Method |
|---|---|
| Enrol a lead in a journey POST /api/v1/journeys/{id}/enroll | TS vf.enrollInJourney({ path: { id }, body })Py vf.enroll_in_journey(id, body) |
Webhook endpoints
| What it does | Method |
|---|---|
| List webhook endpoints GET /api/v1/webhook-endpoints | TS vf.listWebhookEndpoints()Py vf.list_webhook_endpoints() |
| Add a webhook endpoint POST /api/v1/webhook-endpoints | TS vf.createWebhookEndpoint({ body })Py vf.create_webhook_endpoint(body) |
| Get a webhook endpoint GET /api/v1/webhook-endpoints/{id} | TS vf.getWebhookEndpoint({ path: { id } })Py vf.get_webhook_endpoint(id) |
| Change a webhook endpoint PATCH /api/v1/webhook-endpoints/{id} | TS vf.updateWebhookEndpoint({ path: { id }, body })Py vf.update_webhook_endpoint(id, body) |
| Remove a webhook endpoint DELETE /api/v1/webhook-endpoints/{id} | TS vf.deleteWebhookEndpoint({ path: { id } })Py vf.delete_webhook_endpoint(id) |
| Rotate the signing secret POST /api/v1/webhook-endpoints/{id}/rotate-secret | TS vf.rotateWebhookSecret({ path: { id } })Py vf.rotate_webhook_secret(id) |
| Send a test event POST /api/v1/webhook-endpoints/{id}/test | TS vf.sendTestWebhook({ path: { id }, body })Py vf.send_test_webhook(id, body) |
| List deliveries to an endpoint GET /api/v1/webhook-endpoints/{id}/deliveries | TS vf.listWebhookDeliveries({ path: { id }, query })Py vf.list_webhook_deliveries(id, **query) |
Events
| What it does | Method |
|---|---|
| List events GET /api/v1/events | TS vf.listEvents({ query })Py vf.list_events(**query) |
| Get an event, with its deliveries GET /api/v1/events/{id} | TS vf.getEvent({ path: { id } })Py vf.get_event(id) |
| Send an event again POST /api/v1/events/{id}/resend | TS vf.resendEvent({ path: { id }, body })Py vf.resend_event(id, body=None) |
Sandbox
| What it does | Method |
|---|---|
| Simulate an inbound call POST /api/v1/sandbox/inbound-calls | TS vf.simulateInboundCall({ body })Py vf.simulate_inbound_call(body) |
| Move the sandbox on in time POST /api/v1/sandbox/time-travel | TS vf.timeTravel({ body })Py vf.time_travel(body) |
| Simulate a back-order delivery POST /api/v1/sandbox/backorders/{id}/deliver | TS vf.deliverBackorder({ path: { id }, body })Py vf.deliver_backorder(id, body=None) |
| Set the sandbox wallet POST /api/v1/sandbox/wallet | TS vf.setSandboxWallet({ body })Py vf.set_sandbox_wallet(body) |
| Start the sandbox over POST /api/v1/sandbox/reset | TS vf.resetSandbox()Py vf.reset_sandbox() |
Which version am I on?
Each package's version is on npm and PyPI. A new version follows a change to the API: the SDKs are regenerated from the API's own specification, so a new endpoint appears as a new method under the name the reference gives it. Upgrade with npm install @vicifast/api@latest or pip install -U vicifast.