VICIfast

API reference · v1 ·

VICIfast API reference

The VICIfast API is a REST API for phone numbers: search the catalogue, buy and route numbers to your VICIdial servers, place back-orders, read calls with their recordings and transcripts, and follow your wallet, from your own code. JSON over HTTPS, one error shape, signed webhooks, and a sandbox that never charges.

Guides: Quickstart · Receiving webhooks · Testing in the sandbox · SDKs for TypeScript and Python

SDKs: npm install @vicifast/api · pip install vicifast

Authentication

Create a key in API → Keys in the dashboard and send it on every request. A key carries scopes — reading numbers, buying them, releasing them, transcribing calls, seeing billing and managing webhooks are separate grants — and can be limited to the IP addresses you use. The number, order, call and billing endpoints need an approved termination account.

curl https://vicifast.com/api/v1/numbers \
  -H "Authorization: Bearer vf_live_…"

Test mode

Keys that start vf_test_ work against a sandbox: a pretend $100.00 wallet, a made-up catalogue (555-0100 to 555-0199 in every area code; numbers ending 00 or 11 are premium at $25.00) and one server, srv_sandbox. Prices are your real ones. Nothing real is bought, charged or pointed, and every response carries X-VICIfast-Mode: test. A sandbox back-order delivers half of each line at once and leaves the rest open, so you can try tracking and cancelling. With the sandbox:simulate scope, a test key can also make things happen: an inbound call, a month passing, a back-order filling, a low wallet — each sending its webhook to your test endpoints (see Sandbox). Start over with POST /api/v1/sandbox/reset.

Errors

Every error has the same shape. Branch on code; the message is for people and may change. Every response carries an X-Request-Id to quote to support.

{ "error": { "code": "insufficient_funds",
             "message": "That comes to $5.00 and the wallet holds $2.10. …",
             "details": { "needed_cents": 500, "balance_cents": 210 } } }
CodeStatusMeaning
invalid_request400Malformed input; `param` names the field.
unauthorized401No API key, or not a valid one.
insufficient_funds402The wallet cannot cover it. Nothing was charged; top up and retry with the same Idempotency-Key.
forbidden403The key lacks the scope this endpoint needs.
ip_not_allowed403The key is restricted to other addresses.
account_not_approved403The account has no approved termination account yet. Test keys still work.
purchasing_disabled403Number purchases are turned off for this account. Numbers it owns keep working; test keys still work.
test_mode_unsupported403This endpoint has no sandbox.
sandbox_only403A sandbox simulation: use a test key (vf_test_…). Nothing real can be simulated.
not_found404No such thing on this account.
conflict409A request with this Idempotency-Key is still running, or the thing changed state.
numbers_unavailable409Those numbers are no longer for sale, or there are not enough. `details` says which.
unprocessable422Understood but refused (an Idempotency-Key reused for a different request, a server that cannot take numbers).
premium_price_not_accepted422A premium number named without accept_premium_price_cents equal to its price.
limit_exceeded422It would charge more than your max_total_cents or max_charge_cents. Nothing happened.
rate_limited429Over 600 requests a minute on this key. Wait Retry-After seconds.
internal_error500Our fault. Quote X-Request-Id to support.
service_unavailable503Something we depend on did not answer. Retry shortly.

Retries and Idempotency-Key

Requests that spend or refund money — buying, releasing, placing and cancelling back-orders, transcribing a call — need an Idempotency-Key header: any unique string, sent again unchanged when you retry. A retry of a request that succeeded gets the original answer back (with Idempotent-Replayed: true) and nothing happens twice. A request refused with a 4xx changed nothing, so the same key may be retried once the cause is fixed. Keys are remembered for 24 hours.

Pagination

Lists answer { "data": […], "has_more": true, "next_cursor": "…" }. Pass cursor=next_cursor for the next page, and limit (up to 500) for its size.

Rate limits

600 requests a minute per key, on every endpoint. Over it, the answer is 429 with a Retry-After header.

Numbers

What you hold, the catalogue, buying and releasing.

get/api/v1/numbers

List your numbers

The numbers this account holds (or held), newest first.

Needs the numbers:read scope.

Try it in the playground →

Parameters

status"active" | "released" | "all" · queryDefault active.
type"local" | "toll_free" · query
routed"true" | "false" · queryPointed somewhere, or not.
area_codestring · queryOnly numbers in this area code (or toll-free prefix).
order_idstring · queryOnly numbers from this order.
limitinteger · queryPage size, up to 500.
cursorstring · querynext_cursor from the previous page.

200 · A page of numbers.

datarequiredPhoneNumber[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/numbers?status=active&limit=50' \
  -H "Authorization: Bearer $VICIFAST_KEY"
patch/api/v1/numbers

Change many numbers at once

One change — label, notes, auto_renew, routing (with CID groups) — to up to 500 of your numbers. Every one must be a number you hold now, or nothing changes and the answer lists the ones that are not.

Needs the numbers:write scope.

Try it in the playground →

Request body

labelstring | null
notesstring | null
auto_renewboolean
routingobject | nullWhere it rings: a server and in-group, or {"sip_endpoints": true}. null unpoints it; it stays yours and keeps billing.
numbersrequiredstring[]Up to 500 of your numbers. Every one must be a number you hold now, or nothing changes.

200 · Changed. warnings says what did not fully land (a carrier update, a CID group).

datarequiredBulkUpdateResult
warningsstring[]
curl -X PATCH 'https://vicifast.com/api/v1/numbers' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numbers":["+13055550101","+13055550102"],"routing":{"server_id":"srv_…","in_group":"SALES","cid_groups":["FL_LOCAL"]},"auto_renew":true}'
get/api/v1/numbers/available

Search numbers for sale

Numbers in stock, at your own price, in the order a quantity purchase takes them: never-owned first. Premium numbers only with include_premium=true.

Needs the numbers:read scope.

Try it in the playground →

Parameters

type"local" | "toll_free" · query
area_codestring · queryFor toll-free, the prefix: 800, 833, 844…
statestring · queryTwo-letter state.
containsstring · queryDigits, or letters read off a keypad, anywhere in the number: 7777, or FLOWERS.
ends_withstring · queryDigits or keypad letters the number must END with: 0000, or SALES.
npa_nxxstring · queryArea code and exchange together: 305555.
rate_centerstring · queryThe rate center (in practice, the town) the number belongs to, as the numbering plan names it: MIAMI, NEW YORK CITY. Case does not matter.
include_premium"true" | "false" · queryDefault false.
sort"recommended" | "number" | "number_desc" · queryrecommended (the default): never-owned numbers first, as a quantity purchase takes them. number / number_desc: by the digits.
limitinteger · queryPage size, up to 500.
cursorstring · querynext_cursor from the previous page.

200 · A page of numbers for sale.

datarequiredAvailableNumber[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/numbers/available?area_code=305&ends_with=SALES&sort=number' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/numbers/purchase

Buy numbers

Pays from the wallet: the first month of each number (plus any setup fee) now, then monthly. All of them or none. Name the numbers, or ask for a quantity matching criteria. A premium number is bought only when named with accept_premium_price_cents equal to its monthly price. Each number has a three-month minimum term. dry_run returns the exact charge and buys nothing; without it an Idempotency-Key is required.

Needs the numbers:buy scope.

Try it in the playground →

Parameters

Idempotency-Keystring · header

Request body

numbers(string | object)[]Buy these exact numbers. All of them or none. Premium numbers need accept_premium_price_cents.
numberrequiredstringAny common North American form.
accept_premium_price_centsintegerRequired for a premium number: its monthly price, as you were shown it.
quantityintegerOr buy this many matching the criteria, chosen for you (never premium). All of them or none.
type"local" | "toll_free"With quantity: local or toll-free.
area_codestringWith quantity: the area code (toll-free: the prefix).
statestringWith quantity: two-letter state.
containsstringWith quantity: digits or keypad letters the numbers must contain.
ends_withstringWith quantity: digits or keypad letters the numbers must end with.
npa_nxxstringWith quantity: area code and exchange.
rate_centerstringWith quantity: the rate center.
max_total_centsintegerRefuse the purchase if it would charge more than this now.
ring_onobjectPoint the new numbers here straight away.
server_idrequiredstring
in_grouprequiredstring
dry_runbooleanReturn exactly what the purchase would charge and buy nothing. Needs no Idempotency-Key. Not a reservation.

200 · With dry_run: what it would charge. Nothing bought.

datarequiredPurchaseQuote

201 · Bought. The numbers are yours.

datarequiredPurchaseResult
curl -X POST 'https://vicifast.com/api/v1/numbers/purchase' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"quantity":5,"area_code":"305","max_total_cents":1000,"ring_on":{"server_id":"srv_…","in_group":"SALES"}}'
post/api/v1/numbers/release

Give numbers back

Inside the three-month minimum term, the rest of the term is charged per number. dry_run returns that charge and changes nothing; the same call without it releases. All of them or none. Needs an Idempotency-Key unless dry_run.

Needs the numbers:release scope.

Try it in the playground →

Parameters

Idempotency-Keystring · header

Request body

numbersrequiredstring[]
dry_runbooleanReturn exactly what releasing would charge, and release nothing.
max_charge_centsintegerRefuse if the minimum-term charge would be more than this.

200 · Released (or, with dry_run, what releasing would cost).

datarequiredReleaseResult
curl -X POST 'https://vicifast.com/api/v1/numbers/release' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"numbers":["+13055550101"],"dry_run":true}'
get/api/v1/numbers/{number}

Get a number

Needs the numbers:read scope.

Try it in the playground →

Parameters

numberrequiredstring · pathThe number, in any common form (+13055551234 URL-encoded as %2B13055551234).

200 · The number.

datarequiredPhoneNumber
curl 'https://vicifast.com/api/v1/numbers/%2B13055550101' \
  -H "Authorization: Bearer $VICIFAST_KEY"
patch/api/v1/numbers/{number}

Update a number

Label it, write notes, turn auto-renew on or off, or change where it rings: routing points it at one of your servers and an in-group, and null unpoints it (it stays yours and keeps billing). With auto-renew off a number is released at its first renewal after the minimum term.

Needs the numbers:write scope.

Try it in the playground →

Parameters

numberrequiredstring · pathThe number, in any common form (+13055551234 URL-encoded as %2B13055551234).

Request body

labelstring | null
notesstring | null
auto_renewboolean
routingobject | nullWhere it rings: a server and in-group, or {"sip_endpoints": true}. null unpoints it; it stays yours and keeps billing.

200 · The number, as it is now.

datarequiredPhoneNumber
curl -X PATCH 'https://vicifast.com/api/v1/numbers/%2B13055550101' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label":"Miami sales","auto_renew":false,"routing":{"server_id":"srv_…","in_group":"SALES"}}'
get/api/v1/servers

List your servers

Where numbers can be pointed: use a server id in routing or ring_on.

Needs the numbers:read scope.

Try it in the playground →

200 · Your servers.

datarequiredServer[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/servers' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/servers/{id}/cid-groups

List a server's CID groups

The caller-ID pools on one of your servers that its campaigns dial out from, read from a snapshot of the server taken every minute.

Needs the numbers:read scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · Every CID group on the server.

datarequiredCidGroup[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/servers/:id/cid-groups' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/servers/{id}/cid-groups/{group}

Get a CID group, with its numbers

Needs the numbers:read scope.

Try it in the playground →

Parameters

idrequiredstring · path
grouprequiredstring · path

200 · The group.

curl 'https://vicifast.com/api/v1/servers/:id/cid-groups/:group' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/servers/{id}/cid-groups/{group}/numbers

Put numbers into a CID group

Written to the server straight away. The area code column is filled the way the group's type needs: the number's area code, its state, or NONE in a rotation. Numbers the server will not take come back in skipped.

Needs the numbers:write scope.

Try it in the playground →

Parameters

idrequiredstring · path
grouprequiredstring · path

Request body

numbersrequiredstring[]
descriptionstringDefault "VICIfast DID".

200 · Added.

datarequiredCidGroupAdded
curl -X POST 'https://vicifast.com/api/v1/servers/:id/cid-groups/:group/numbers' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numbers":["+13055550101","+13055550102"]}'
post/api/v1/servers/{id}/cid-groups/{group}/numbers/remove

Take numbers out of a CID group

The numbers stay yours and keep ringing; campaigns using the group stop calling out from them.

Needs the numbers:write scope.

Try it in the playground →

Parameters

idrequiredstring · path
grouprequiredstring · path

Request body

numbersrequiredstring[]

200 · Removed; not_in_group lists any that were not in it.

datarequiredCidGroupRemoved
curl -X POST 'https://vicifast.com/api/v1/servers/:id/cid-groups/:group/numbers/remove' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numbers":["+13055550102"]}'

Orders

Purchases and back-orders.

get/api/v1/orders

List orders

Batches of numbers you received: a purchase, or a back-order as it fills.

Needs the orders:read scope.

Try it in the playground →

Parameters

status"open" | "filled" | "routed" | "closed" · query
limitinteger · queryPage size, up to 500.
cursorstring · querynext_cursor from the previous page.

200 · A page of orders.

datarequiredOrder[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/orders' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/orders/{id}

Get an order, with its numbers

Needs the orders:read scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · The order.

datarequiredOrder
curl 'https://vicifast.com/api/v1/orders/:id' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/backorders

List back-orders

Needs the orders:read scope.

Try it in the playground →

Parameters

status"open" | "filled" | "cancelled" | "expired" · query
limitinteger · queryPage size, up to 500.
cursorstring · querynext_cursor from the previous page.

200 · A page of back-orders.

datarequiredBackorder[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/backorders' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/backorders

Place a back-order

Ask for numbers by area code (or toll-free prefix). Paid up front at your price; numbers are delivered as stock arrives, each line counted separately. Cancel any time to get back what has not arrived.

Needs the orders:write scope.

Try it in the playground →

Parameters

Idempotency-Keystring · header

Request body

type"local" | "toll_free"Default local.
linesobject[]Required for local. A toll-free order may give quantity instead.
area_coderequiredstringArea code; for toll-free, the prefix (800, 833…).
quantityrequiredinteger
quantityintegerToll-free only, in place of lines: this many numbers from whichever toll-free prefix is in stock.
notesstringFree text sent with the order, for the people filling it.
allow_nearbybooleanWhen an area code runs short, a nearby one in the same state will do (any toll-free prefix, on a toll-free order).
ring_onobjectPoint the numbers here as they arrive.
server_idrequiredstring
in_grouprequiredstring
max_total_centsintegerRefuse the order if it would charge more than this now.
dry_runbooleanReturn exactly what the order would charge and place nothing. Needs no Idempotency-Key.

200 · With dry_run: what it would charge. Nothing placed.

datarequiredBackorderQuote

201 · Placed and paid.

datarequiredBackorder
curl -X POST 'https://vicifast.com/api/v1/backorders' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"lines":[{"area_code":"305","quantity":20},{"area_code":"786","quantity":10}],"allow_nearby":true}'
get/api/v1/backorders/{id}

Get a back-order

Needs the orders:read scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · The back-order.

datarequiredBackorder
curl 'https://vicifast.com/api/v1/backorders/:id' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/backorders/{id}/cancel

Cancel a back-order

Stops waiting and refunds the numbers that have not arrived. Delivered numbers stay yours.

Needs the orders:write scope.

Try it in the playground →

Parameters

idrequiredstring · path
Idempotency-Keyrequiredstring · headerRequired. Any unique string (a UUID is ideal). Retry with the same key and the original result is returned instead of the work being done twice.

200 · Cancelled and refunded.

datarequiredobject
objectrequired"backorder"
idrequiredstring
statusrequired"open" | "filled" | "cancelled" | "expired"
typerequired"local" | "toll_free"
quantityrequiredinteger
filledrequiredinteger
allow_nearbyrequiredboolean
price_each_centsrequiredintegerPrepaid per number; the most any will cost.
paid_centsrequiredintegerUS cents.
refunded_centsrequiredintegerUS cents.
linesrequiredobject[]
area_coderequiredstringArea code, or toll-free prefix.
staterequiredstring | null
quantityrequiredinteger
filledrequiredinteger
ring_onrequiredobject | null
server_idrequiredstring
in_grouprequiredstring
order_idrequiredstring | nullThe order the delivered numbers are in, once the first arrives.
notesrequiredstring | nullWhat you sent with the order.
created_atrequiredstring
cancelled_atrequiredstring | null
refunded_now_centsrequiredinteger
curl -X POST 'https://vicifast.com/api/v1/backorders/:id/cancel' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Calls

Call records and recordings.

get/api/v1/calls

List calls

Outbound calls you made and inbound calls to your numbers, newest first, from the last 90 days at most.

Needs the calls:read scope.

Try it in the playground →

Parameters

direction"inbound" | "outbound" · query
numberstring · queryInbound calls to this one of your numbers.
answered"true" | "false" · query
fromstring · queryOnly calls from this number.
tostring · queryOnly calls to this number.
window"1h" | "24h" | "7d" | "30d" | "90d" · queryDefault 7d. Calls are kept for at least 90 days.
limitinteger · queryPage size, up to 500.
cursorstring · querynext_cursor from the previous page.

200 · A page of calls.

datarequiredCall[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/calls?direction=inbound&window=7d' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/calls/{id}

Get a call

Needs the calls:read scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · The call.

datarequiredCall
curl 'https://vicifast.com/api/v1/calls/:id' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/calls/export

Export calls as CSV

The same calls and the same fields as List calls, as a CSV file of up to 50,000 rows. When there are more, X-Next-Cursor names where the next file starts (pass it as cursor).

Needs the calls:read scope.

Try it in the playground →

Parameters

direction"inbound" | "outbound" · query
numberstring · queryInbound calls to this one of your numbers.
answered"true" | "false" · query
fromstring · queryOnly calls from this number.
tostring · queryOnly calls to this number.
window"1h" | "24h" | "7d" | "30d" | "90d" · queryDefault 7d. Calls are kept for at least 90 days.
limitinteger · queryRows per file, up to 50,000 (default 10,000).
cursorstring · querynext_cursor from the previous page.
curl 'https://vicifast.com/api/v1/calls/export?window=90d&direction=outbound&from=%2B13055550101' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/calls/{id}/transcript

Get a call's transcript

A transcript made before, free to read as often as you like.

Needs the calls:read scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · The transcript.

datarequiredTranscript
curl 'https://vicifast.com/api/v1/calls/:id/transcript' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/calls/{id}/transcript

Transcribe a call

Transcribes the call's recording, with the speakers told apart. 5¢ a minute of call (rounded up), charged to the wallet once: a call already transcribed returns its transcript free. dry_run says what it would cost. If the transcription fails, nothing is charged.

Needs the calls:transcribe scope.

Try it in the playground →

Parameters

idrequiredstring · path
Idempotency-Keystring · header

Request body

dry_runbooleanSay what it would cost, and change nothing.
max_charge_centsintegerRefuse if it would cost more than this.

200 · With dry_run: what it would cost. Or the transcript made before, free.

201 · Made and charged.

datarequiredTranscript
curl -X POST 'https://vicifast.com/api/v1/calls/:id/transcript' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"max_charge_cents":50}'

Billing

The wallet.

get/api/v1/billing/balance

Get the wallet balance

Needs the billing:read scope.

Try it in the playground →

200 · The wallet.

datarequiredBalance
curl 'https://vicifast.com/api/v1/billing/balance' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/billing/transactions

List wallet transactions

Needs the billing:read scope.

Try it in the playground →

Parameters

typestring · queryOne type or several, comma-separated: charge_did_purchase,topup_stripe
direction"credit" | "debit" · query
fromstring · query
tostring · query
limitinteger · queryPage size, up to 500.
cursorstring · querynext_cursor from the previous page.

200 · A page of transactions, newest first.

datarequiredTransaction[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/billing/transactions' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/billing/renewals

List upcoming renewals

What the wallet will be charged, and when: your numbers and wallet-billed servers.

Needs the billing:read scope.

Try it in the playground →

Parameters

daysinteger · queryDefault 30.

200 · Upcoming renewals, soonest first.

datarequiredRenewal[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/billing/renewals' \
  -H "Authorization: Bearer $VICIFAST_KEY"

Messaging

SMS and MMS from your numbers.

post/api/v1/messages

Send a message

SMS (text) or MMS (media_urls) from one of your numbers. Accepted with 202: queued, not yet at the carrier; watch status for sent and delivered. An Idempotency-Key makes a retry return the same message (200) instead of sending twice. Not in the sandbox.

Needs the messages:send scope.

Try it in the playground →

Parameters

Idempotency-Keystring · header

Request body

fromrequiredstringOne of your numbers that can send messages.
torequiredstring
textstring
media_urlsstring[]For MMS.
send_atstringSchedule it instead of sending now.

200 · The message an Idempotency-Key already sent.

datarequiredMessage

202 · Accepted and queued.

datarequiredMessage
curl -X POST 'https://vicifast.com/api/v1/messages' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"from":"+13055550101","to":"+12125559999","text":"Your appointment is at 3pm."}'
get/api/v1/messages

List messages

Sent and received, newest first. Pages also carry X-Next-Cursor, as they always have.

Needs the messages:read scope.

Try it in the playground →

Parameters

direction"inbound" | "outbound" · query
status"queued" | "scheduled" | "sending" | "sent" | "delivered" | "undelivered" | "failed" | "suppressed" | "received" · query
conversation_idstring · query
limitinteger · queryPage size, up to 100. Default 25.
cursorstring · querynext_cursor from the previous page.

200 · A page of messages.

datarequiredMessage[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/messages' \
  -H "Authorization: Bearer $VICIFAST_KEY"

Journeys

Starting a journey for a lead from outside the dialer.

post/api/v1/journeys/{id}/enroll

Enrol a lead in a journey

Starts a journey's calls and texts for a phone number, from a web form or a CRM. A number the dialer does not know yet becomes a lead first, in the journey's list (or list_id), after the dialer's own DNC checks. The journey must allow API enrolment in its settings. Already enrolled is not an error: enrolled comes back false with the reason. Not in the sandbox.

Needs the journeys:enroll scope.

Try it in the playground →

Parameters

idrequiredstring · path
Idempotency-Keystring · header

Request body

phone_numberrequiredstringA North American number: the dialer stores ten digits.
list_idintegerThe dialer list a new lead is added to. Default: the journey's own list. Not used when the number is already a lead.
first_namestring
last_namestring
address1string
address2string
citystring
statestring
postal_codestring
country_codestring
emailstring
commentsstring
vendor_lead_codestring
source_idstring
titlestring
fieldsobjectAny other lead fields, by their VICIdial names.
idempotency_keystringDeprecated: send the Idempotency-Key header instead. Still honoured.

200 · Enrolled, or why not.

datarequiredJourneyEnrollment
curl -X POST 'https://vicifast.com/api/v1/journeys/:id/enroll' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"phone_number":"+13055550123","first_name":"Ana","email":"ana@example.com","fields":{"lead_source":"web form"}}'

SIP endpoints

Your own PBX or SBC: the addresses your numbers ring when they are not on a server we host.

get/api/v1/sip-endpoints

List your SIP endpoints

Every endpoint with the addresses it resolves to and their health, how calls are shared, and whether new numbers ring them.

Needs the numbers:read scope.

Try it in the playground →

200 · Your endpoints and settings.

datarequiredSipEndpointList
curl 'https://vicifast.com/api/v1/sip-endpoints' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/sip-endpoints

Add a SIP endpoint

A public IP address or a DNS name. Refused: private and reserved addresses, ours and our carriers’, and an address another account uses.

Needs the numbers:write scope.

Try it in the playground →

Request body

hostrequiredstring
portintegerDefault 5060, or 5061 for tls.
transport"udp" | "tcp" | "tls"Default udp.
weightintegerDefault 1.
enabledbooleanDefault true.
notestring

201 · Added.

datarequiredSipEndpoint
curl -X POST 'https://vicifast.com/api/v1/sip-endpoints' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"host":"pbx.example.com","port":5060,"transport":"udp","weight":70}'
patch/api/v1/sip-endpoints/{id}

Change a SIP endpoint

Its host, port, transport, weight or note, or switch it off and on.

Needs the numbers:write scope.

Try it in the playground →

Parameters

idrequiredstring · path

Request body

hoststring
portintegerDefault 5060, or 5061 for tls.
transport"udp" | "tcp" | "tls"Default udp.
weightintegerDefault 1.
enabledbooleanDefault true.
notestring

200 · Changed.

datarequiredSipEndpoint
curl -X PATCH 'https://vicifast.com/api/v1/sip-endpoints/:id' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'
delete/api/v1/sip-endpoints/{id}

Remove a SIP endpoint

Numbers keep ringing the endpoints that are left.

Needs the numbers:write scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · Removed.

datarequiredDeletedSipEndpoint
curl -X DELETE 'https://vicifast.com/api/v1/sip-endpoints/:id' \
  -H "Authorization: Bearer $VICIFAST_KEY"
put/api/v1/sip-endpoint-settings

Change how SIP endpoints are used

How calls are shared, whether new numbers ring the endpoints, and whether existing numbers move there now.

Needs the numbers:write scope.

Try it in the playground →

Request body

share"roundrobin" | "weighted"roundrobin: every address takes calls equally. weighted: in proportion to weight.
new_numbers_ring_endpointsboolean
move_unpointedbooleanWith new_numbers_ring_endpoints true: also point every number that rings nowhere at the endpoints.
move_server_pointedbooleanWith new_numbers_ring_endpoints true: also move every number that rings a server to the endpoints.

200 · The endpoints and settings, as they now are.

datarequiredSipEndpointList
curl -X PUT 'https://vicifast.com/api/v1/sip-endpoint-settings' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"share":"weighted","new_numbers_ring_endpoints":true,"move_unpointed":true}'

Webhook endpoints

Where events are sent, their signing secrets, and what was delivered.

get/api/v1/webhook-endpoints

List webhook endpoints

This key's mode only: a live key sees live endpoints, a test key test ones.

Needs the webhooks:read scope.

Try it in the playground →

200 · Every endpoint, oldest first.

datarequiredWebhookEndpoint[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/webhook-endpoints' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/webhook-endpoints

Add a webhook endpoint

Up to 10 per account. The endpoint gets this key's mode: one made with a test key receives the sandbox's events. The secret is in the answer; keep it.

Needs the webhooks:write scope.

Try it in the playground →

Request body

urlrequiredstringhttps only, and on the public internet: private and loopback addresses are refused.
descriptionstring
eventsrequired("numbers.arrived" | "number.released" | "number.renewal_failed" | "call.inbound.completed" | "call.outbound.completed" | "wallet.low" | "backorder.filled" | "number.routing_failed" | "wallet.topped_up" | "ai_call.completed")[]
wallet_low_threshold_centsintegerRequired with wallet.low.

201 · Added.

curl -X POST 'https://vicifast.com/api/v1/webhook-endpoints' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/vicifast","events":["numbers.arrived","number.routing_failed","wallet.low"],"wallet_low_threshold_cents":5000}'
get/api/v1/webhook-endpoints/{id}

Get a webhook endpoint

Needs the webhooks:read scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · The endpoint.

datarequiredWebhookEndpoint
curl 'https://vicifast.com/api/v1/webhook-endpoints/:id' \
  -H "Authorization: Bearer $VICIFAST_KEY"
patch/api/v1/webhook-endpoints/{id}

Change a webhook endpoint

Its address, events, threshold, description, or switch it off and on.

Needs the webhooks:write scope.

Try it in the playground →

Parameters

idrequiredstring · path

Request body

urlstringhttps only, and on the public internet: private and loopback addresses are refused.
descriptionstring | null
events("numbers.arrived" | "number.released" | "number.renewal_failed" | "call.inbound.completed" | "call.outbound.completed" | "wallet.low" | "backorder.filled" | "number.routing_failed" | "wallet.topped_up" | "ai_call.completed")[]Replaces the list.
enabledboolean
wallet_low_threshold_centsinteger | null

200 · Changed.

datarequiredWebhookEndpoint
curl -X PATCH 'https://vicifast.com/api/v1/webhook-endpoints/:id' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'
delete/api/v1/webhook-endpoints/{id}

Remove a webhook endpoint

Its undelivered events are dropped with it.

Needs the webhooks:write scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · Removed.

datarequiredobject
objectrequired"webhook_endpoint"
idrequiredstring
deletedrequiredtrue
curl -X DELETE 'https://vicifast.com/api/v1/webhook-endpoints/:id' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/webhook-endpoints/{id}/rotate-secret

Rotate the signing secret

A new secret for this endpoint, used from the next delivery on. The old one stops working at once, so update your receiver straight after.

Needs the webhooks:write scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · The new secret.

datarequiredWebhookSecret
curl -X POST 'https://vicifast.com/api/v1/webhook-endpoints/:id/rotate-secret' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/webhook-endpoints/{id}/test

Send a test event

Sends a sample of one event type to this endpoint now, signed like any other, and says what came back. The sample has test: true in its data and is tried once.

Needs the webhooks:write scope.

Try it in the playground →

Parameters

idrequiredstring · path

Request body

typerequired"numbers.arrived" | "number.released" | "number.renewal_failed" | "call.inbound.completed" | "call.outbound.completed" | "wallet.low" | "backorder.filled" | "number.routing_failed" | "wallet.topped_up" | "ai_call.completed"Which event to send a sample of.

200 · Sent; delivered says whether it arrived.

datarequiredWebhookTestResult
curl -X POST 'https://vicifast.com/api/v1/webhook-endpoints/:id/test' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"numbers.arrived"}'
get/api/v1/webhook-endpoints/{id}/deliveries

List deliveries to an endpoint

Newest first: what was sent, what came back, and what is still being retried.

Needs the webhooks:read scope.

Try it in the playground →

Parameters

idrequiredstring · path
state"pending" | "retrying" | "delivered" | "gave_up" · query
limitinteger · queryPage size, up to 500.
cursorstring · querynext_cursor from the previous page.

200 · A page of deliveries.

datarequiredWebhookDelivery[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/webhook-endpoints/:id/deliveries' \
  -H "Authorization: Bearer $VICIFAST_KEY"

Events

The events of the last 30 days, and sending one again.

get/api/v1/events

List events

The events of the last 30 days, newest first, in this key's mode — each one exactly as its webhook body. An event is recorded when at least one endpoint was subscribed to it at the time; catch up on what your receiver missed from here.

Needs the webhooks:read scope.

Try it in the playground →

Parameters

typestring · queryOne type or several, comma-separated: numbers.arrived,number.released
fromstring · query
tostring · query
limitinteger · queryPage size, up to 500.
cursorstring · querynext_cursor from the previous page.

200 · A page of events.

datarequiredEvent[]
has_morerequiredboolean
next_cursorrequiredstring | null
curl 'https://vicifast.com/api/v1/events?type=numbers.arrived,number.released&limit=50' \
  -H "Authorization: Bearer $VICIFAST_KEY"
get/api/v1/events/{id}

Get an event, with its deliveries

Needs the webhooks:read scope.

Try it in the playground →

Parameters

idrequiredstring · path

200 · The event.

curl 'https://vicifast.com/api/v1/events/:id' \
  -H "Authorization: Bearer $VICIFAST_KEY"
post/api/v1/events/{id}/resend

Send an event again

Queues the event again, to one endpoint or to every endpoint subscribed to it now, and starts sending at once. Each is a new delivery with a new X-VICIfast-Delivery id; the event id is the same, so a receiver that keeps the event ids it has handled can tell it is a repeat.

Needs the webhooks:write scope.

Try it in the playground →

Parameters

idrequiredstring · path

Request body

endpoint_idstringOnly to this endpoint. Left out: to every endpoint that is on and subscribed to the type now.

202 · Queued; the deliveries are on their way.

datarequiredResendResult
curl -X POST 'https://vicifast.com/api/v1/events/:id/resend' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endpoint_id":"we_…"}'

Sandbox

Test keys only: make a call ring, time pass, a back-order fill or the wallet run low, and see what your code does.

post/api/v1/sandbox/inbound-calls

Simulate an inbound call

A caller rings one of your sandbox numbers. The call appears in List calls (with a recording you can transcribe, when answered) and call.inbound.completed goes to your test endpoints.

Needs the sandbox:simulate scope.

Try it in the playground →

Request body

numberrequiredstringOne of the sandbox numbers you hold.
fromstringThe caller. Default +12025550123.
answeredbooleanDefault true.
duration_sintegerHow long it lasted, if answered. Default 60.

201 · The call.

datarequiredCall
curl -X POST 'https://vicifast.com/api/v1/sandbox/inbound-calls' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"number":"+13055550100","from":"+12125559999","duration_s":95}'
post/api/v1/sandbox/time-travel

Move the sandbox on in time

Up to 400 days. Renewals that fall due are charged to the pretend wallet, oldest first; a number with auto-renew off past its minimum term is released; a renewal the wallet cannot pay fails. Each sends its webhook to your test endpoints.

Needs the sandbox:simulate scope.

Try it in the playground →

Request body

daysrequiredintegerHow far to move the sandbox on. Renewals that fall due are charged to the pretend wallet, numbers with auto-renew off past their minimum term are released, and a renewal the wallet cannot pay fails — each with its webhook.

200 · What happened.

datarequiredTimeTravelResult
curl -X POST 'https://vicifast.com/api/v1/sandbox/time-travel' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"days":31}'
post/api/v1/sandbox/backorders/{id}/deliver

Simulate a back-order delivery

Numbers arrive against an open sandbox back-order: numbers.arrived, and backorder.filled once everything asked for is in.

Needs the sandbox:simulate scope.

Try it in the playground →

Parameters

idrequiredstring · path

Request body

quantityintegerHow many to deliver now. Default: everything still to come.

200 · The back-order, after the delivery.

datarequiredBackorder
curl -X POST 'https://vicifast.com/api/v1/sandbox/backorders/:id/deliver' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"quantity":5}'
post/api/v1/sandbox/wallet

Set the sandbox wallet

Sets the pretend balance. Dropping below a test endpoint’s wallet.low threshold sends wallet.low; raising it sends wallet.topped_up. Set it low, then time-travel, to see a renewal fail.

Needs the sandbox:simulate scope.

Try it in the playground →

Request body

balance_centsrequiredintegerThe pretend balance to set. Below a test endpoint’s wallet.low threshold sends wallet.low; raising it sends wallet.topped_up.

200 · The pretend wallet.

datarequiredBalance
curl -X POST 'https://vicifast.com/api/v1/sandbox/wallet' \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"balance_cents":150}'
post/api/v1/sandbox/reset

Start the sandbox over

No numbers, no orders, and a fresh $100 pretend wallet.

Needs the sandbox:simulate scope.

Try it in the playground →

200 · Reset.

datarequiredSandboxReset
curl -X POST 'https://vicifast.com/api/v1/sandbox/reset' \
  -H "Authorization: Bearer $VICIFAST_KEY"

Webhooks

Add endpoints in API → Webhooks in the dashboard or with the endpoints API — several if you like, each with its own events and its own signing secret. We POST each event as JSON and retry anything but a 2xx after 1, 5, 15, 60 and 240 minutes. Skip an event you have already handled by its id, which is the same on every retry and on a resend; X-VICIfast-Delivery names one delivery, and a resend is a new one. Missed some? The last 30 days are in List events. No receiver yet? Point an endpoint at the hosted test inbox in API → Webhooks and see each request, its headers and whether its signature checks out.

{
  "id": "cmg…",
  "object": "event",
  "type": "numbers.arrived",
  "livemode": true,
  "created_at": "2026-10-03T20:00:00.000Z",
  "data": { … }
}

Checking a request is from us

X-VICIfast-Signature: t=…,v1=… carries an HMAC-SHA256 of `${t}.${body}` with the endpoint’s secret. Compare it in constant time and refuse a t more than five minutes old.

import crypto from 'node:crypto';

function fromVicifast(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const want = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return v1?.length === want.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(want));
}
numbers.arrived

Numbers are yours: bought, delivered against a back-order, or assigned.

data

sourcerequired"purchase" | "backorder" | "assigned"
order_idrequiredstring | null
backorder_idrequiredstring | null
numbersrequiredstring[]
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.
number.released

A number left your account.

data

numberrequiredstringE.164, with the plus.
reasonrequired"customer" | "auto_renew_off" | "account"
charge_centsrequiredinteger
released_atrequiredstring
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.
number.renewal_failed

A renewal could not be paid (sent once per number per billing month; the charge is retried every hour).

data

numberrequiredstringE.164, with the plus.
amount_centsrequiredinteger
due_atrequiredstring
reasonrequired"insufficient_funds" | "error"
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.
call.inbound.completed

An inbound call to one of your numbers finished.

data

objectrequired"call"
idrequiredstringThe SIP call id.
directionrequired"inbound" | "outbound"
fromrequiredstringThe caller, E.164 when it is a North American number.
torequiredstring
numberrequiredstring | nullInbound: which of your numbers it arrived on.
started_atrequiredstring
answered_atrequiredstring | null
ended_atrequiredstring | null
answeredrequiredboolean
duration_srequiredinteger
billable_srequiredinteger
sip_coderequiredinteger | null
hangup_causerequiredstring
charge_nanosrequiredintegerWhat the call cost, in billionths of a dollar (2000000 = $0.002).
recordingrequiredbooleanA recording can be fetched with GET /v1/calls/{id}/recording.
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.
call.outbound.completed

An outbound call you made finished, answered or not. A dialer makes many; each is one delivery.

data

objectrequired"call"
idrequiredstringThe SIP call id.
directionrequired"inbound" | "outbound"
fromrequiredstringThe caller, E.164 when it is a North American number.
torequiredstring
numberrequiredstring | nullInbound: which of your numbers it arrived on.
started_atrequiredstring
answered_atrequiredstring | null
ended_atrequiredstring | null
answeredrequiredboolean
duration_srequiredinteger
billable_srequiredinteger
sip_coderequiredinteger | null
hangup_causerequiredstring
charge_nanosrequiredintegerWhat the call cost, in billionths of a dollar (2000000 = $0.002).
recordingrequiredbooleanA recording can be fetched with GET /v1/calls/{id}/recording.
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.
wallet.low

The wallet dropped below the endpoint's threshold.

data

objectrequired"balance"
balance_centsrequiredinteger
threshold_centsrequiredinteger
currencyrequired"usd"
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.
backorder.filled

Every number a back-order asked for has arrived (sent once, after the numbers.arrived for the last delivery).

data

backorder_idrequiredstring
order_idrequiredstringThe order the numbers were delivered into.
quantityrequiredinteger
refunded_centsrequiredintegerRefunded in all because numbers cost less than the price paid up front.
filled_atrequiredstring
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.
number.routing_failed

A number could not be pointed where you asked (a server, or your SIP endpoints), so it will not ring there yet. Point it again (PATCH the number) once the cause is fixed.

data

numberrequiredstringE.164, with the plus.
server_idrequiredstringEmpty when it was being pointed at your SIP endpoints.
route_kindrequiredstringingroup, or endpoints for your SIP endpoints.
targetrequiredstringThe in-group (or other target) it was to ring.
stagerequired"platform" | "switch" | "server"platform: the server had no address or VICIfast trunk, or pointing failed before anything was sent. switch: our switch did not take the change. server: your server did not take it after three tries (is it running?).
errorrequiredstring | null
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.
wallet.topped_up

Money was added to the wallet: by card, PayPal, crypto, or by us.

data

objectrequired"transaction"
idrequiredstring
typerequiredstring
amount_centsrequiredintegerSigned: positive credits the wallet.
balance_after_centsrequiredintegerUS cents.
descriptionrequiredstring | null
reference_idrequiredstring | null
created_atrequiredstring
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.
ai_call.completed

One of your AI agents finished a call: its outcome, the answers it collected, and a summary. Sent a minute or so after the call, once the summary is written. Only for agents with "Notify my CRM" on; never for test calls.

data

objectrequired"ai_call"
idrequiredstring
agentrequiredobject
idrequiredstring
namerequiredstring
directionrequired"inbound" | "outbound"
moderequired"live" | "voicemail"voicemail: the agent left a message on an answering machine.
caller_numberrequiredstring | nullThe other party: who called in, or the lead that was dialled.
lead_idrequiredstring | nullThe VICIdial lead id, when there is one.
started_atrequiredstring | null
ended_atrequiredstring | null
duration_secondsrequiredinteger | null
outcomerequiredstring | nullWhat the agent decided the call was: qualified, appointment, not_interested, callback, do_not_call, wrong_number, answered, message, and so on.
vicidial_statusrequiredstring | nullThe status written on the lead in VICIdial.
transferred_torequiredstring | nullThe in-group the caller was handed to, when the agent transferred them.
answersrequiredobjectWhat the agent collected, keyed by the question you set up.
callback_atrequiredstring | null
appointment_atrequiredstring | null
do_not_callrequiredbooleanThe person asked not to be called again.
summaryrequiredstring | null
sentimentrequired"positive" | "neutral" | "negative" | null
flagsrequiredstring[]Problems found when the call was reviewed: ai_not_disclosed, dnc_ignored, promise_made, talked_over, rude, wrong_info.
recordingrequiredbooleanA recording is kept for this call (90 days).
testtrueOnly on a sample sent with "send test": the values are made up. Real events never carry it.

Objects

What the endpoints and events above return, described once. Every object carries an object field naming its kind.

AvailableNumber

Fields

objectrequired"available_number"
numberrequiredstringE.164, with the plus.
typerequired"local" | "toll_free"
staterequiredstring | null
area_coderequiredstring | null
rate_centerrequiredstring | null
freshrequiredbooleanNever owned by anybody before.
premiumrequiredboolean
monthly_price_centsrequiredintegerYour price, from your own terms.
setup_price_centsrequiredintegerUS cents.

Backorder

Fields

objectrequired"backorder"
idrequiredstring
statusrequired"open" | "filled" | "cancelled" | "expired"
typerequired"local" | "toll_free"
quantityrequiredinteger
filledrequiredinteger
allow_nearbyrequiredboolean
price_each_centsrequiredintegerPrepaid per number; the most any will cost.
paid_centsrequiredintegerUS cents.
refunded_centsrequiredintegerUS cents.
linesrequiredobject[]
area_coderequiredstringArea code, or toll-free prefix.
staterequiredstring | null
quantityrequiredinteger
filledrequiredinteger
ring_onrequiredobject | null
server_idrequiredstring
in_grouprequiredstring
order_idrequiredstring | nullThe order the delivered numbers are in, once the first arrives.
notesrequiredstring | nullWhat you sent with the order.
created_atrequiredstring
cancelled_atrequiredstring | null

BackorderQuote

Fields

objectrequired"backorder_quote"
dry_runrequiredtrue
charge_centsrequiredintegerUS cents.
quantityrequiredinteger
price_each_centsrequiredintegerUS cents.
balance_centsrequiredintegerUS cents.

Balance

Fields

objectrequired"balance"
balance_centsrequiredintegerUS cents.
currencyrequired"usd"
credit_limit_centsrequiredintegerHow far below zero renewals may take the wallet so numbers keep ringing. Purchases need the funds.
low_balance_alertrequiredobject
enabledrequiredboolean
threshold_centsrequiredinteger | null
auto_top_uprequiredobject
enabledrequiredboolean

BulkUpdateResult

Fields

objectrequired"bulk_update"
updatedrequiredinteger
numbersrequiredPhoneNumber[]

Call

Fields

objectrequired"call"
idrequiredstringThe SIP call id.
directionrequired"inbound" | "outbound"
fromrequiredstringThe caller, E.164 when it is a North American number.
torequiredstring
numberrequiredstring | nullInbound: which of your numbers it arrived on.
started_atrequiredstring
answered_atrequiredstring | null
ended_atrequiredstring | null
answeredrequiredboolean
duration_srequiredinteger
billable_srequiredinteger
sip_coderequiredinteger | null
hangup_causerequiredstring
charge_nanosrequiredintegerWhat the call cost, in billionths of a dollar (2000000 = $0.002).
recordingrequiredbooleanA recording can be fetched with GET /v1/calls/{id}/recording.

CidGroup

Fields

objectrequired"cid_group"
idrequiredstring
typerequired"area_code" | "state" | "rotation"area_code: a caller ID is picked to match the area code dialled. state: to match the state. rotation: every caller ID in turn.
notesrequiredstring | null
numbers_countrequiredinteger
synced_atrequiredstringWhen the server was last read.

CidGroupAdded

Fields

objectrequired"cid_group_change"
group_idrequiredstring
addedrequiredinteger
skippedrequiredobject[]
numberrequiredstringE.164, with the plus.
errorrequiredstring

CidGroupRemoved

Fields

objectrequired"cid_group_change"
group_idrequiredstring
removedrequiredinteger
not_in_grouprequiredstring[]

CidGroupWithNumbers

Fields

objectrequired"cid_group"
idrequiredstring
typerequired"area_code" | "state" | "rotation"area_code: a caller ID is picked to match the area code dialled. state: to match the state. rotation: every caller ID in turn.
notesrequiredstring | null
numbers_countrequiredinteger
synced_atrequiredstringWhen the server was last read.
numbersrequiredobject[]
numberrequiredstring
area_coderequiredstringWhat VICIdial matches on: an area code, a state, or NONE in a rotation.
activerequiredboolean
descriptionrequiredstring | null

DeletedSipEndpoint

Fields

objectrequired"sip_endpoint"
idrequiredstring
deletedrequiredtrue

Event

Fields

idrequiredstring
objectrequired"event"
typerequiredstring
livemoderequiredboolean
created_atrequiredstring
datarequiredobjectShaped by the type; see Webhooks below. Samples sent with "send test" have test: true.

EventWithDeliveries

Fields

idrequiredstring
objectrequired"event"
typerequiredstring
livemoderequiredboolean
created_atrequiredstring
datarequiredobjectShaped by the type; see Webhooks below. Samples sent with "send test" have test: true.
deliveriesrequiredWebhookDelivery[]

JourneyEnrollment

Fields

objectrequired"journey_enrollment"
enrolledrequiredboolean
lead_idrequiredinteger
created_leadrequiredbooleantrue when the number was not a lead yet.
reasonstringWhy not, when enrolled is false (already in it, re-entry rules).

Message

Fields

idrequiredstring
objectrequired"message"
directionrequired"inbound" | "outbound"
statusrequired"queued" | "scheduled" | "sending" | "sent" | "delivered" | "undelivered" | "failed" | "suppressed" | "received"sent means the carrier took it; only delivered means it landed. undelivered is the common outcome of carrier filtering.
fromrequiredstring
torequiredstring
textrequiredstring | null
media_urlsrequiredstring[]
segmentsrequiredinteger | null
cost_centsrequiredinteger | null
currencyrequiredstring | null
error_coderequiredstring | null
error_detailrequiredstring | null
conversation_idrequiredstring | null
send_atrequiredstring | nullWhen a scheduled message goes.
sent_atrequiredstring | null
delivered_atrequiredstring | null
failed_atrequiredstring | null
created_atrequiredstring | null

Order

Fields

objectrequired"order"
idrequiredstring
statusrequired"open" | "filled" | "routed" | "closed"open: still arriving (from a back-order). filled: all delivered. routed: all pointed at one place. closed: every number released.
typerequired"local" | "toll_free" | null
quantityrequiredinteger
active_numbersrequiredinteger
criteriarequiredobject
staterequiredstring | null
area_coderequiredstring | null
containsrequiredstring | null
backorder_idrequiredstring | null
routingrequiredobject | nullWhere the whole order was pointed, when it was pointed as one.
server_idrequiredstring
in_grouprequiredstring | null
created_atrequiredstring
numbersPhoneNumber[]On GET /v1/orders/{id} only.

PhoneNumber

Fields

objectrequired"phone_number"
idrequiredstring
numberrequiredstringE.164, with the plus.
typerequired"local" | "toll_free"
premiumrequiredbooleanA memorable number with its own price.
statusrequired"active" | "released"
labelrequiredstring | null
notesrequiredstring | null
staterequiredstring | null
area_coderequiredstring | null
rate_centerrequiredstring | null
monthly_price_centsrequiredintegerUS cents.
currencyrequired"usd"
auto_renewrequiredboolean
at_next_renewalrequired"charge" | "release" | nullWhat happens on next_renewal_at. With auto_renew off the number is released at the first renewal after its three-month minimum term; before that, months are still charged.
purchased_atrequiredstring
min_term_ends_atrequiredstring
next_renewal_atrequiredstring | null
released_atrequiredstring | null
release_charge_centsrequiredinteger | null
order_idrequiredstring | null
routingrequiredobject | nullWhere it rings on a server. Null when it rings nowhere or rings your SIP endpoints.
server_idrequiredstring
server_hostnamerequiredstring
route_kindrequired"ingroup" | "campaign" | "extension" | "ivr"
targetrequiredstringThe in-group (or other target) calls land on.
staterequired"pending" | "applied" | "failed"Whether the server has taken the change yet.
errorrequiredstring | null
sip_endpointsrequiredbooleanIt rings your own SIP endpoints (see /api/v1/sip-endpoints); routing is then null.

PurchaseQuote

Fields

objectrequired"purchase_quote"
dry_runrequiredtrue
charge_centsrequiredintegerWhat the purchase would charge now.
balance_centsrequiredintegerUS cents.
numbersrequiredobject[]
numberrequiredstringE.164, with the plus.
typerequired"local" | "toll_free"
premiumrequiredboolean
monthly_price_centsrequiredintegerUS cents.
setup_price_centsrequiredintegerUS cents.

PurchaseResult

Fields

objectrequired"purchase"
order_idrequiredstring
charged_centsrequiredintegerUS cents.
balance_centsrequiredintegerYour wallet after the charge.
numbersrequiredPhoneNumber[]
ringingrequiredobject | nullHow pointing went, when ring_on was given.
okrequiredboolean
messagerequiredstring

ReleaseResult

Fields

objectrequired"release"
dry_runrequiredboolean
charge_centsrequiredintegerThe balance of each number's three-month minimum term. Zero once the term is served.
numbersrequiredobject[]
numberrequiredstringE.164, with the plus.
releasedrequiredboolean
charge_centsrequiredintegerUS cents.
errorrequiredstring | null

Renewal

Fields

objectrequired"renewal"
kindrequired"number" | "server"
descriptionrequiredstringThe number, or the server hostname.
due_atrequiredstring
actionrequired"charge" | "release"release: a number with auto-renew off, past its minimum term, ends that day.
amount_centsrequiredintegerUS cents.

ResendResult

Fields

objectrequired"event_resend"
event_idrequiredstring
deliveriesrequiredWebhookDelivery[]

SandboxReset

Fields

objectrequired"sandbox"
resetrequiredtrue

Server

Fields

objectrequired"server"
idrequiredstring
hostnamerequiredstring
statusrequiredstring
accepts_numbersrequiredbooleanHas an address, so numbers can be pointed at it. They arrive over the VICIfast trunk, installed on the server when needed.

SipEndpoint

Fields

objectrequired"sip_endpoint"
idrequiredstring
hostrequiredstringA public IP address or a DNS name.
portrequiredinteger
transportrequired"udp" | "tcp" | "tls"
weightrequiredintegerIts share under weighted sharing, 1 to 1000.
enabledrequiredbooleanOff takes it out of the rotation without forgetting it. Its addresses can still send your outbound calls.
noterequiredstring | null
addressesrequiredobject[]What the host resolves to, checked every five minutes. Every address takes part in the rotation. Empty: the name does not resolve yet.
iprequiredstring
healthrequired"up" | "down" | "unknown" | "refused"up / down: answering our health checks, or stopped. unknown: has never answered one — common for a PBX that ignores OPTIONS, and it still gets calls. refused: never used, see 'refused'.
refusedrequired"" | "private" | "ours" | "other_account"Why a refused address is never used: private (not reachable from the internet), ours (one of our addresses or our carriers), other_account (in use on another account).

SipEndpointList

Fields

objectrequired"sip_endpoint_list"
sharerequired"roundrobin" | "weighted"roundrobin: every address takes calls equally. weighted: in proportion to weight.
new_numbers_ring_endpointsrequiredbooleanNumbers bought from now on ring these endpoints unless the purchase names a server.
datarequiredSipEndpoint[]

TimeTravelResult

Fields

objectrequired"time_travel"
daysrequiredinteger
renewals_chargedrequiredinteger
charged_centsrequiredintegerUS cents.
releasedrequiredstring[]
renewal_failedrequiredstring[]

Transaction

Fields

objectrequired"transaction"
idrequiredstring
typerequiredstring
amount_centsrequiredintegerSigned: positive credits the wallet.
balance_after_centsrequiredintegerUS cents.
descriptionrequiredstring | null
reference_idrequiredstring | null
created_atrequiredstring

Transcript

Fields

objectrequired"transcript"
call_idrequiredstring
languagerequiredstring | null
textrequiredstring
utterancesrequiredobject[]
speakerrequiredstringA, B, … as told apart in the audio; ? when unsure.
start_srequirednumber
end_srequirednumber
textrequiredstring
duration_srequiredinteger
charged_centsrequiredintegerWhat making it cost, once.
created_atrequiredstring

TranscriptQuote

Fields

objectrequired"transcript_quote"
dry_runrequiredtrue
call_idrequiredstring
minutesrequiredinteger
price_per_minute_centsrequiredintegerUS cents.
charge_centsrequiredintegerUS cents.
balance_centsrequiredintegerUS cents.

WebhookDelivery

Fields

objectrequired"webhook_delivery"
idrequiredstringSent as X-VICIfast-Delivery.
endpoint_idrequiredstring
event_idrequiredstring
event_typerequiredstring
testrequiredbooleanA sample sent with "send test". Tried once.
staterequired"pending" | "retrying" | "delivered" | "gave_up"retrying: failed, and tried again after 1, 5, 15, 60 and 240 minutes. gave_up: every attempt failed, or the receiver refused it outright (a 4xx other than 408 or 429).
attemptsrequiredinteger
last_statusrequiredinteger | null
last_errorrequiredstring | null
created_atrequiredstring
delivered_atrequiredstring | null
next_attempt_atrequiredstring | null

WebhookEndpoint

Fields

objectrequired"webhook_endpoint"
idrequiredstring
urlrequiredstring
descriptionrequiredstring | null
eventsrequired("numbers.arrived" | "number.released" | "number.renewal_failed" | "call.inbound.completed" | "call.outbound.completed" | "wallet.low" | "backorder.filled" | "number.routing_failed" | "wallet.topped_up" | "ai_call.completed")[]
enabledrequiredbooleanfalse: nothing is sent to it.
livemoderequiredbooleantrue: your account's real events. false: the sandbox's, from test keys. Follows the key that created it.
wallet_low_threshold_centsrequiredinteger | nullwallet.low fires once when the balance drops below this, and again only after it has been back above.
last_statusrequiredinteger | nullHTTP status of the last attempt.
last_errorrequiredstring | null
last_sent_atrequiredstring | null
failing_sincerequiredstring | nullSet while deliveries are failing; cleared by the next one that succeeds.
created_atrequiredstring

WebhookEndpointWithSecret

Fields

objectrequired"webhook_endpoint"
idrequiredstring
urlrequiredstring
descriptionrequiredstring | null
eventsrequired("numbers.arrived" | "number.released" | "number.renewal_failed" | "call.inbound.completed" | "call.outbound.completed" | "wallet.low" | "backorder.filled" | "number.routing_failed" | "wallet.topped_up" | "ai_call.completed")[]
enabledrequiredbooleanfalse: nothing is sent to it.
livemoderequiredbooleantrue: your account's real events. false: the sandbox's, from test keys. Follows the key that created it.
wallet_low_threshold_centsrequiredinteger | nullwallet.low fires once when the balance drops below this, and again only after it has been back above.
last_statusrequiredinteger | nullHTTP status of the last attempt.
last_errorrequiredstring | null
last_sent_atrequiredstring | null
failing_sincerequiredstring | nullSet while deliveries are failing; cleared by the next one that succeeds.
created_atrequiredstring
secretrequiredstringSigns every delivery (X-VICIfast-Signature). Shown when the endpoint is made and when the secret is rotated; keep it.

WebhookSecret

Fields

objectrequired"webhook_secret"
endpoint_idrequiredstring
secretrequiredstringThe new secret. The old one stops working from the next delivery.

WebhookTestResult

Fields

objectrequired"webhook_test"
event_idrequiredstring
delivery_idrequiredstring
deliveredrequiredbooleanWhether your endpoint answered 2xx.
statusrequiredinteger | null
errorrequiredstring | null

Common questions

How do I authenticate with the VICIfast API?

Send an API key from API → Keys in the dashboard as Authorization: Bearer vf_live_…. Each key has scopes (reading numbers, buying them, releasing them, billing and webhooks are separate grants) and can be limited to the IP addresses you use.

Is there a sandbox for testing?

Yes. Keys that start vf_test_ work against a sandbox with a pretend $100 wallet, a made-up catalogue and one server. Nothing real is bought or charged, and the sandbox can simulate an inbound call, time passing, a back-order delivery or a low wallet.

What are the rate limits?

600 requests a minute per key, on every endpoint. A request over the limit is answered 429 with a Retry-After header saying when to try again.

How are webhooks signed and retried?

Every delivery carries X-VICIfast-Signature: an HMAC-SHA256 of the timestamp and the body with the endpoint’s secret. Refuse one more than five minutes old. Anything but a 2xx is retried after 1, 5, 15, 60 and 240 minutes.

How do I retry a purchase safely?

Send an Idempotency-Key header on every request that spends or refunds money. Retrying with the same key returns the original answer instead of buying twice; keys are remembered for 24 hours.

Are there SDKs for the VICIfast API?

Yes: @vicifast/api for TypeScript and Node.js (npm install @vicifast/api) and vicifast for Python (pip install vicifast). Both are generated from the OpenAPI document, and both send Idempotency-Keys, retry safely, walk pages and check webhook signatures for you.

Is there an OpenAPI specification?

Yes: an OpenAPI 3.1 document at /api-docs/openapi.json, generated from the same schemas the API validates requests with, for code generators and API clients.