API v3

VoicePro Plus API v3

Check numbers, order and route phone numbers, run IoT SIMs and read your whole account from your own code.Test keys that never charge, an MCP server for AI assistants, and a real response for every call.

Open in Claude Open in ChatGPT

Your first call in 10 minutes

STEP 1

Create a test key · 2 min

Your email, the code we send it, your key. No account needed. Already a customer? Make keys in the portal under API and webhooks.

Expected resultA key starting vpkey_test_. It never sees your account and is never charged.

STEP 2

Check a number · 1 min

Terminal
curl -X POST "https://api.voicepro.plus/v3/dips" \
  -H "Authorization: Bearer vpkey_test_YOUR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"number":"+447700900001"}'

Expected resultHTTP 201 with the range check and the last port date 12/03/2025: this test number was ported.

STEP 3

Go live · 1 min

With an Enterprise account, make a live key in the portal and swap it in (no account yet? talk to us). Nothing else changes. Then pick your next call from the recipes or the reference.

Expected resultThe same code, real answers. Every answer carries a request-id to quote to support.

Why build on VoicePro

Recipes

Common jobs, call by call. Each step links to its call in the reference, with code and a real response.

Check a number before you route a call

Stop calls to numbers that are not in service, and route ported numbers to the right network.

  1. GET /dips/products See the checks on sale and their prices.
  2. POST /dips Check the number: range (valid, line type) and last port date.
  3. GET /dips/usage Watch this month's checks and spend.

Buy a UK number and send its calls to your system

Add numbers from your own app without opening the portal.

  1. GET /number_offers Find a number and its price.
  2. POST /number_orders Order it, paid from your balance.
  3. GET /numbers See it on your account.
  4. PATCH /numbers/{id} Ring your app or forward to another number.

Change 1,000 IoT SIMs at once

Switch a whole fleet on or off, or change its settings, in one job.

  1. GET /iot/sims Pick the SIMs.
  2. POST /iot/bulk_jobs Start one change for up to 5,000 SIMs.
  3. GET /iot/bulk_jobs/{id} Follow the job (or wait for the iot.bulk_job_done webhook).
  4. GET /iot/bulk_jobs/{id}/results See what changed, SIM by SIM.

Send IoT SIM traffic to your own platform

Route a SIM's traffic through your connector, and check it on the SIM's network log.

  1. POST /iot/connectors Add your connector.
  2. POST /iot/connectors/{name}/test Test it: an HTTPS connector gets a labelled test POST, and you see recent delivery errors.
  3. PATCH /iot/sims/{id}/connector Send one SIM's traffic through it.
  4. GET /iot/sims/{id}/events Read that SIM's network log: attaches, data sessions and detaches.

Pull your voice rates and traffic every day

Feed your own billing and reporting from the same numbers we bill on.

  1. GET /trunks Your trunk settings.
  2. GET /rates Current and scheduled rates.
  3. GET /traffic Totals and a time series for the range you ask for.
  4. GET /traffic/destinations Traffic by destination.

Reconcile what you owe

Match your balance, spend and invoices in your finance system.

  1. GET /balance Your prepaid balance now.
  2. GET /spend This month's spend by service, the projection and your spending limit.
  3. GET /invoices Every invoice.
  4. GET /invoices/{id} One invoice with its lines.

Guides

Keys and authentication

Base URL: https://api.voicepro.plus/v3. Send the key as Authorization: Bearer vpkey_... over HTTPS only.

A key belongs to one account and can only see that account: an id from another account answers 404. Revoking a key in the portal stops it at once. A key can carry an expiry date and a list of allowed IP addresses. The portal lists every request each key made in the last 30 days, under API and webhooks, then Requests.

Lists and paging

Every list takes limit (default 50, up to 500) and cursor. Pass back next_cursor unchanged for the next page; it is null on the last page. Lists are newest first and pages are stable.

Responses are JSON with snake_case fields and public ids such as sim_... and num_.... Money is in minor units with its currency. Reads built from stored copies carry data_as_of, the time the copy was taken.

Safe retries

Every POST, PATCH and DELETE takes an Idempotency-Key header (8-128 characters). The first answer is stored for 24 hours and replayed for a retry with the same key and body, so a network timeout never orders, charges or changes anything twice.

For calls that cost money (number checks, number check files and number orders), a retry while the first attempt is still running answers 409 idempotency_key_in_use: wait and retry with the same key.

Rate limits

10 requests a second, bursts of 25 per key, and 30 requests a second, bursts of 75, shared by all of an account's keys. Every keyed answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; a 429 carries Retry-After.

A service your account does not have answers 404 service_not_enabled. A data service that is briefly down answers 503, never an empty list.

Test keys

Tick Test key when you create a key and it starts vpkey_test_. A test key gets the documented example for every call, so you can build and run your whole integration before going live. It never sees your account, never changes anything and is never charged. Changes still need an Idempotency-Key and every call still needs the key's permission, exactly as live. Number checks answer from these test numbers:

+447700900000Valid UK mobile, never ported
+447700900001Valid UK mobile, ported to another network
+447700900002Not a valid number (range not in service)
+447700900003No answer from either check (as when a network is unreachable): an empty result, nothing charged

Errors

Every error has the same shape. code is stable and safe to branch on; message is for people.

Error shape
{
  "error": {
    "type": "authentication",
    "code": "invalid_api_key",
    "message": "This API key is not recognised.",
    "request_id": "req_..."
  }
}
missing_api_key401: No Authorization header, or not a Bearer key.
invalid_api_key401: The key is not recognised.
api_key_revoked401: The key was revoked in the portal.
api_key_expired401: The key passed the expiry date set on it.
missing_scope403: The key lacks the scope this endpoint needs; param names the scope.
ip_not_allowed403: The key has an IP allowlist and this request came from elsewhere.
account_not_active403: The workspace is suspended or closed.
invalid_parameter400: A query parameter or body field has a bad value; param names it.
missing_parameter400: A required parameter or field is missing; param names it.
unknown_parameter400: A parameter or field that this endpoint does not accept; param names it.
invalid_json400: The body is not valid JSON.
invalid_cursor400: The cursor is malformed, expired or from a different list.
invalid_time_range400: from or to is not an ISO 8601 date-time, or to is not after from.
range_too_long400: The time range is longer than this endpoint allows (stated in the endpoint).
method_not_allowed405: The path exists but not with this method.
route_not_found404: No such path in v3.
resource_not_found404: No object with that id on this account.
service_not_enabled404: The account does not have this service (voice trunk, IoT, numbers); param names it.
idempotency_key_reused409: This Idempotency-Key was used within 24 hours with a different request.
idempotency_key_in_use409: A request with this Idempotency-Key is still running, or its outcome at a partner is not yet known (message: outcome pending); retry shortly with the same key. If a charged request never finished, the message says so: check your balance and orders before sending it again with a new key.
state_conflict409: The object is not in a state that allows this action (for example resuming an active SIM).
insufficient_balance402: Prepaid funds or the account's order limit do not cover this charged action (a dip, an SMS or an order).
rate_limited429: Too many requests for this key or for the whole account (the message says which); wait Retry-After seconds.
upstream_unavailable503: A network partner or data service behind this endpoint is unavailable.
network_action_unavailable503: This network action is not switched on for the account yet (for example SIM suspend).
internal_error500: Our fault. Quote the request_id to support.
service_unavailable503: API v3 itself is briefly unavailable (for example a configuration problem on our side); the rest of VoicePro is unaffected. Retry later.
account_locked423: The account is locked pending a Trust & Safety review; writes, including charged actions, are refused until it is cleared. Reads still work.

API reference

Every path is relative to https://api.voicepro.plus/v3. Each call shows its fields, a working example in cURL, JavaScript or Python, and a real response.

Account

SIMs

IoT SIMs

IoT connectors

Numbers

Voice trunk

Activity

Billing

Dips

Webhooks

Webhooks

Add webhooks by API (POST /webhooks, with a key that has the webhooks:write scope) or in the portal, under Account, then API and webhooks (owners and admins). A webhook has an HTTPS address and one or more event types (events, up to 20), and gets a signing secret shown once.

We POST JSON with X-VoicePro-Event and two signatures made with your secret. VoicePro-Signature is t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of the time, a dot and the raw body: check it, and refuse a time more than 5 minutes old so an old message cannot be replayed. X-VoicePro-Signature is the HMAC-SHA256 of the raw body alone, as before.

Each delivery has a 10-second timeout. One that does not answer 2xx is tried again, after 15 minutes, 1, 3, 6, 12 and 24 hours, for up to 3 days; X-VoicePro-Delivery-Attempt says which try it is. Every event carries an id, so a receiver can ignore one it has already handled. The last 50 deliveries for each webhook are in GET /webhooks/{id}/deliveries and the portal, and 30 days of events in GET /events; POST /events/{id}/resend sends one again. POST /webhooks/{id}/test (or the portal's Test button) sends a signed test delivery to your address. A SIM suspended or reactivated, a number transfer update and events from the IoT network are sent straight away; spending, data cap, roaming, answer rate, invoice, incident and alert rule events are checked every 15 minutes.

Message body
{
  "id": "<event id, evt_...>",
  "event": "<event code>",
  "type": "event",
  "occurredAt": "<ISO date-time>",
  "workspaceId": "<your workspace id>",
  "data": { ... }
}
Check the signature (Node.js)
const header = String(req.headers["voicepro-signature"] ?? "");
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const expected = crypto.createHmac("sha256", SECRET).update(`${parts.t}.${rawBody}`).digest("hex");
const sent = String(parts.v1 ?? "");
const ok = fresh && sent.length === expected.length && crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected));
if (!ok) return reject();
spend.thresholdSpending passed an alert threshold

data: pct, used, limit, currency, period

sim.data_capA SIM passed 80% or 100% of its data cap

data: simId, label, pct, usedBytes, capGb

sim.roamingA SIM started using data abroad

data: simId, label, countries

sim.suspendedA SIM was suspended or reactivated

data: Mobile SIMs: simId, label, state ("suspended" | "active"). IoT SIMs: iot: true, simId, label, activated

trunk.asr_dropVoice answer rate dropped sharply

data: asr, asrSameTimeYesterday, calls, window

invoice.issuedA new invoice was issued

data: id, period, totalGbp, dueAt

invoice.overdueAn invoice became overdue

data: id, period, totalGbp, dueAt

porting.updatedA number transfer changed status

data: id, status, transferDate, actionNeeded

incident.openedA service incident affects your services

data: id, title, kind, impact, startedAt, statusPage

alert.ruleOne of your alert rules fired

data: rule, name, trigger, alerts

iot.attachedAn IoT SIM attached to a network

data: simId, iccid, label, time, network, networkType

iot.detachedAn IoT SIM detached from a network

data: simId, iccid, label, time, network, networkType

iot.authenticationAn IoT SIM tried to authenticate on a network (allowed or refused)

data: simId, iccid, label, time, network, networkType

iot.data_limitAn IoT SIM reached its data alert or data limit

data: simId, iccid, label, time, network, networkType, bytes, reason, dataLimit { used, total, period, alertAt, resetsAt }

iot.cost_alertAn IoT SIM reached its cost alert

data: simId, iccid, label, time, network, networkType, bytes

iot.smsAn IoT device sent an SMS

data: simId, iccid, label, time, network, networkType, text

iot.sms_statusAn SMS to or from an IoT device changed status

data: simId, iccid, label, time, network, networkType, status, direction ("to_device" | "from_device")

iot.bulk_job_doneA bulk change to IoT SIMs finished

data: jobId, total, succeeded, failed, change, finishedAt

number.watch_changedA number you watch changed (network, porting, line status or risk)

data: watch_id, phone_number, label, at, changes { field: { from, to } }

Tools

Connect an AI assistant (MCP)

Our MCP server at https://api.voicepro.plus/mcp gives AI assistants one tool for every call in the reference, using your own key. A tool call is the same API call: the key's permissions, rate limits and access log all apply, and a test key keeps it on example data.

Set up
# Claude Code
claude mcp add --transport http voicepro https://api.voicepro.plus/mcp \
  --header "Authorization: Bearer vpkey_test_YOUR_KEY"

# Cursor (.cursor/mcp.json)
{ "mcpServers": { "voicepro": { "url": "https://api.voicepro.plus/mcp",
  "headers": { "Authorization": "Bearer vpkey_test_YOUR_KEY" } } } }

Versioning and uptime

The version is in the address: https://api.voicepro.plus/v3. Within v3 we only add: new calls, new optional fields and new values. Your code should ignore fields and values it does not know.

A breaking change only ever ships as a new major version, and the previous version keeps working for at least 12 months after we announce it, with a Sunset header on its responses.

Our uptime target is 99.9% each calendar month, reported on the status page.

Changelog

Follow it in any feed reader: RSS.

2026-10-29

Cancel a number, and number watches

  • POST /numbers/{id}/cancel: no further charges; the number keeps working to the end of the month already paid, then is released.
  • Number watches: POST /number_watches re-checks a number daily and records every change (porting, network, line status, risk); GET /number_watches/{id}/events lists them.
  • New webhook event number.watch_changed, sent when a watched number changes.
  • The number check product is now named as it is everywhere else: code nrv (VPX-NRV, Number Range Verify).
  • POST /dip_batches: check a file of up to 20,000 numbers, paid from your balance before it starts; anything not run is refunded.
  • SIMs: POST /sims/{id}/suspend and /reactivate; IoT: POST /iot/sims/{id}/sms sends a text to the device.
  • POST /port_requests: ask to transfer numbers in. POST /invoices/{id}/payment_link: a secure payment link for an open invoice. GET /invoices/{id}/pdf: the VAT invoice.
  • Webhooks v2: several event types per webhook, a timestamped signature (VoicePro-Signature), retries for up to 3 days, and 30 days of events in GET /events with POST /events/{id}/resend.
  • New key scopes: sms:send, porting:write and billing:write.
2026-10-28

API v3

  • The API moves to api.voicepro.plus/v3, with a new home at voicepro.plus/developers: get started, recipes, guides and a searchable reference with cURL, JavaScript and Python for every call.
  • Test keys (vpkey_test_...): every call answers with example data, and number checks answer from fixed test numbers. Never charged.
  • MCP server at api.voicepro.plus/mcp: AI assistants get one tool for every call, using your key.
  • Downloads: OpenAPI 3.1 spec, Postman collection, TypeScript and Python clients, and the whole reference as plain text for AI tools.
  • Portal: API and webhooks now lists every request each key made in the last 30 days.
  • DELETE /iot/connectors/{name} is now listed in the reference (it was live but not shown).
  • New calls: GET /trunks/channels (channels in use per trunk, now and at peak), GET /fraud/incidents (your fraud incidents and losses, with the previous period) and GET /risk_ranges (search the high-risk and premium-rate ranges our network screens against).
  • New call: GET /number_requirements (what you must supply, per country and number type, before a number goes live).
  • Webhooks by API: list the events, create, change, pause, delete and test webhooks, and read each one's deliveries (GET/POST /webhooks and /webhooks/{id}).
  • IoT SIMs: change one SIM (PATCH /iot/sims/{id}: name, on or off, IMEI lock, data limit, technologies), its eSIM state and SMS history, and your network lists with a SIM put on one.

Coming next

/iot/usageData across all IoT SIMs

Get your key

API keys and webhooks come with every Enterprise account. No Enterprise account yet? Talk to us.