SecretVoIP API

Send SMS and read delivery reports, list SIP lines and calls, top up SIP lines and read your balance from your own software. The API runs the same checks, prices and money rules as the portal.

Getting started

  1. 1. API access is turned on per account. Open a support ticket in the portal and ask for it; once it is on, the client account sees API keys in the menu.
  2. 2. Create a key, tick the access it needs and copy it. The full key is shown only once.
  3. 3. Send it with every request in the Authorization header.
Base URL
https://secretvoip.com/portal-api/v1

Requests and answers are JSON over HTTPS. Money is in US dollars: the main balance, SIP balance and SMS prices are strings with 4 decimals ("0.0752"), call rates are strings with 6 decimals, and call charges and the usage summary are numbers. Times are ISO 8601 in UTC. Every answer has "ok": true or "ok": false.

Your first request
curl "https://secretvoip.com/portal-api/v1/account/balance" \
  -H "Authorization: Bearer $SVP_KEY"

Keys and access

A key looks like svp_ followed by 40 letters and digits. Send it as Authorization: Bearer svp_… (or in an X-API-Key header). Only the client account can create keys, at most 10 active at a time. A key works as that account: the same balance, the same routes and the same prices. Revoke a key in the portal and it stops at once.

AccessWhat the key can do
smsSend SMS, get prices, read sends and delivery reports, cancel scheduled sends
voipSIP lines, call records, call rates, top up SIP lines from the main balance
accountRead-only: balance, subscription and usage
none tickedEverything above

Keep keys on your server. Never put a key in a web page or an app that others can download.

Errors and limits

A failed request answers with an HTTP status and { "ok": false, "error": "…" }, plus a code when there is one (the list below; other errors have no code or "code": null and the text says what is wrong). For SMS, a 400 or 409 also lists every problem in problems (English) and problemCodes (stable keys).

StatusCodeMeaning
400NOT_SENDABLEThe send has a problem (see problems and problemCodes)
400A field is missing or wrong, for example the requestKey or scheduleAt (the error says which)
400MIN_AMOUNTA top-up below $10
401API_KEY_MISSINGNo key was sent, or it is not in the svp_ format
401API_KEY_INVALIDThe key is unknown or was revoked, or its account is no longer active
403API_SCOPEThe key does not have this access
403API_DISABLEDAPI access is turned off for this account (every key of the account stops at once)
403SMS_DISABLEDSMS is not turned on for your account
404NOT_FOUNDNo such endpoint (an unknown item, such as a send id, answers 404 without a code)
409NOT_SENDABLEThe price went up since your quote (problem code sms.problem.priceChanged)
409SUBSCRIPTION_REQUIREDSending needs an active subscription
409INSUFFICIENT_MAINThe main balance is too low for this send or top-up
429RATE_LIMITMore than 120 requests in a minute with one key
429Too many sends in a minute for your account; wait a moment
202Top-up still being checked: ask again with the same requestKey (see Top up SIP lines)
502 / 503Top-up not made, nothing charged

Use a new requestKey for every send and top-up, and the same one when you retry after a timeout: a repeated key is answered with the first result and is never charged twice.

SMS

The flow is quote, then send. Quote with your numbers, text and sender to see the routes and prices; pick a route; quote again with that routeId to get its total; send the same body with a requestKey and that total as expectedTotal.

Full example

Node.js 18+
const BASE = "https://secretvoip.com/portal-api/v1";
const headers = { Authorization: `Bearer ${process.env.SVP_KEY}`, "Content-Type": "application/json" };
const sms = { numbers: "+45 12 34 56 78", message: "Your login code is 4821.", senderId: "SecretVoIP", mode: "world" };

// 1. quote: routes and prices, nothing is sent
const quote = await (await fetch(`${BASE}/sms/quote`, { method: "POST", headers, body: JSON.stringify(sms) })).json();
const route = quote.routes.world.find((r) => r.usable);

// 2. the exact price on that route, then send; the same requestKey is never sent or charged twice
const priced = await (await fetch(`${BASE}/sms/quote`, { method: "POST", headers, body: JSON.stringify({ ...sms, routeId: route.id }) })).json();
const res = await fetch(`${BASE}/sms/send`, {
  method: "POST",
  headers,
  body: JSON.stringify({ ...sms, routeId: route.id, requestKey: "login-58123", expectedTotal: priced.total }),
});
const sent = await res.json();
if (!sent.ok) throw new Error(`${sent.code}: ${sent.error}`);

// 3. delivery report
const report = await (await fetch(`${BASE}/sms/messages/${sent.batch.id}`, { headers })).json();
console.log(report.messages.map((m) => `${m.recipient} ${m.status}`));
Python 3 (requests)
import os, requests

BASE = "https://secretvoip.com/portal-api/v1"
H = {"Authorization": f"Bearer {os.environ['SVP_KEY']}"}
sms = {"numbers": "+45 12 34 56 78", "message": "Your login code is 4821.", "senderId": "SecretVoIP", "mode": "world"}

quote = requests.post(f"{BASE}/sms/quote", json=sms, headers=H, timeout=30).json()
route = next(r for r in quote["routes"]["world"] if r["usable"])
priced = requests.post(f"{BASE}/sms/quote", json={**sms, "routeId": route["id"]}, headers=H, timeout=30).json()

sent = requests.post(f"{BASE}/sms/send", headers=H, timeout=30, json={
    **sms, "routeId": route["id"], "requestKey": "login-58123", "expectedTotal": priced["total"],
}).json()
if not sent["ok"]:
    raise RuntimeError(f"{sent['code']}: {sent['error']}")

report = requests.get(f"{BASE}/sms/messages/{sent['batch']['id']}", headers=H, timeout=30).json()
print([(m["recipient"], m["status"]) for m in report["messages"]])

SMS settings

GET/sms/configscope: sms

Whether SMS is on for your account, your main balance and the limits of one send.

Request
curl "https://secretvoip.com/portal-api/v1/sms/config" \
  -H "Authorization: Bearer $SVP_KEY"
Response 200
{
  "ok": true,
  "enabled": true,
  "canSend": true,
  "lastSenderId": "SecretVoIP",
  "main": "8.8354",
  "hasRoutes": true,
  "paused": false,
  "limits": { "maxRecipients": 5000, "maxSegments": 1, "bulkWarn": 700 }
}

Get a price (quote)

POST/sms/quotescope: sms

Checks the numbers, the text and the sender, and lists every route that can deliver them with its price. Nothing is sent and nothing is charged. Call it first to get a routeId, then send with the same body.

Body fieldTypeAbout
numbersrequiredstring or string[]One or more numbers in international format, with or without + or 00. A string can hold many, split by new lines, commas or semicolons.
messagerequiredstringThe text. One SMS: up to 160 characters of the standard SMS alphabet (€ [ ] { } ^ ~ | and \ count as 2), or 70 if it has emoji or other characters outside it. Longer texts are refused, never split.
senderIdrequiredstringUp to 11 letters, digits, spaces and . _ & -, or a number of 3 to 15 digits.
mode"world" | "numbered" | "country"Which kind of route to use. Default "world".
routeIduuidThe route for mode "world" or "numbered" (from routes.world[].id or routes.numbered[].id).
channelsobjectMode "country": the channel per country code, for example { "45": "<routes.country[].channels[].id>" }.
Request
curl -X POST "https://secretvoip.com/portal-api/v1/sms/quote" \
  -H "Authorization: Bearer $SVP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "numbers": "+45 12 34 56 78",
    "message": "Your login code is 4821.",
    "senderId": "SecretVoIP",
    "mode": "world"
  }'
Response 200
{
  "ok": true,
  "parse": { "valid": 1, "duplicates": 0, "invalidCount": 0, "invalid": [],
             "countries": [{ "cc": "45", "name": "Denmark", "count": 1 }] },
  "segments": { "units": 24, "unicode": false, "segments": 1, "perSegment": 160, "max": 1 },
  "sender": { "ok": true, "problem": null },
  "routes": {
    "world": [
      { "id": "21043c3c-d748-4fdd-9688-0d8294f9c80f", "name": "Standard Worldwide", "section": "world",
        "recommended": false, "priceMin": "0.0752", "priceMax": "0.0752", "missing": [],
        "usable": true, "perMessageTotal": "0.0752" }
    ],
    "country": [{ "cc": "45", "name": "Denmark", "count": 1, "channels": [
      { "id": "…", "name": "Denmark CH1", "price": "0.1153", "usable": true } ] }],
    "numbered": [ … ]
  },
  "total": "0.0000",
  "messages": 0,
  "problems": ["Pick a route."],
  "problemCodes": [{ "key": "sms.problem.pickRoute" }],
  "main": "8.8354"
}

Send SMS

POST/sms/sendscope: sms

Takes every field of the quote (numbers, message, senderId, mode, routeId or channels) plus the fields below. The price is taken from your main balance at once; money for SMS that are not sent comes back automatically.

Body fieldTypeAbout
requestKeyrequiredstringYour own id for this send, 8 to 80 characters (A-Z, a-z, 0-9, _ and -). Sending again with the same key never sends or charges twice; the answer then has "repeated": true.
expectedTotalstringThe "total" of a quote made WITH the same routeId (or channels). If the price went up since, nothing is sent (409, problem code sms.problem.priceChanged).
scheduleAtISO 8601 timeSend later: at least 1 minute and at most 30 days from now.
Request
curl -X POST "https://secretvoip.com/portal-api/v1/sms/send" \
  -H "Authorization: Bearer $SVP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "numbers": "+45 12 34 56 78",
    "message": "Your login code is 4821.",
    "senderId": "SecretVoIP",
    "mode": "world",
    "routeId": "21043c3c-d748-4fdd-9688-0d8294f9c80f",
    "requestKey": "login-58123",
    "expectedTotal": "0.0752"
  }'
Response 200
{
  "ok": true,
  "batch": {
    "id": "8a4e7d9a-8d39-4eb5-a275-fc69d79ec362",
    "kind": "send",
    "senderId": "SecretVoIP",
    "message": "Your login code is 4821.",
    "segments": 1,
    "status": "queued",
    "scheduledAt": null,
    "recipients": 1,
    "reserved": "0.0752",
    "refunded": "0.0000",
    "charged": "0.0752",
    "createdAt": "2026-10-13T13:07:38.447Z",
    "finishedAt": null
  },
  "repeated": false,
  "main": "8.7602"
}

List sends

GET/sms/messagesscope: sms

Your sends, newest first, 20 per page, with a count of messages per status.

Query parameterTypeAbout
pageintegerPage number, from 1. hasMore tells if there is a next page.
Request
curl "https://secretvoip.com/portal-api/v1/sms/messages?page=1" \
  -H "Authorization: Bearer $SVP_KEY"
Response 200
{
  "ok": true,
  "batches": [
    { "id": "8a4e7d9a-8d39-4eb5-a275-fc69d79ec362", "kind": "send", "senderId": "SecretVoIP",
      "message": "Your login code is 4821.", "segments": 1, "status": "done", "scheduledAt": null,
      "recipients": 1, "reserved": "0.0752", "refunded": "0.0000", "charged": "0.0752",
      "createdAt": "2026-10-13T13:07:38.447Z", "finishedAt": "2026-10-13T13:07:38.485Z",
      "counts": { "delivered": 1 } }
  ],
  "hasMore": false,
  "page": 1
}

Delivery report of a send

GET/sms/messages/{id}scope: sms

One send and the status of every message in it (up to 1000).

Request
curl "https://secretvoip.com/portal-api/v1/sms/messages/8a4e7d9a-8d39-4eb5-a275-fc69d79ec362" \
  -H "Authorization: Bearer $SVP_KEY"
Response 200
{
  "ok": true,
  "batch": { "id": "8a4e7d9a-8d39-4eb5-a275-fc69d79ec362", "status": "done", "charged": "0.0752", … },
  "messages": [
    { "id": 286, "recipient": "4512345678", "route": "Standard Worldwide", "segments": 1,
      "price": "0.0752", "status": "delivered", "refunded": false, "reason": null, "ref": 286,
      "text": "Your login code is 4821.", "sentAt": "2026-10-13T13:07:38.467Z",
      "statusAt": "2026-10-13T13:07:40.912Z" }
  ]
}

Cancel a scheduled send

POST/sms/messages/{id}/cancelscope: sms

Only a send with scheduleAt that has not started yet. The money comes back to your main balance at once.

Request
curl -X POST "https://secretvoip.com/portal-api/v1/sms/messages/8a4e7d9a-8d39-4eb5-a275-fc69d79ec362/cancel" \
  -H "Authorization: Bearer $SVP_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
Response 200
{ "ok": true, "main": "8.8354" }

VoIP

SIP lines

GET/voip/sip-linesscope: voip

Your SIP lines with caller ID, registration and IP login. Passwords are never returned.

Request
curl "https://secretvoip.com/portal-api/v1/voip/sip-lines" \
  -H "Authorization: Bearer $SVP_KEY"
Response 200
{
  "ok": true,
  "lines": [
    { "id": 269, "username": "office2", "callerId": "4512345678", "active": true,
      "registered": true, "lastRegistrationAt": "2026-10-13T13:07:42.484Z", "userAgent": null,
      "description": "Office", "assignedMemberId": null, "assignedMemberName": null,
      "isOwnerLine": false, "createdAt": null,
      "ipLogin": { "ip": "203.0.113.10", "state": "active", "callerIdMode": "server" } }
  ]
}

Call records

GET/voip/callsscope: voip

Your calls, newest first, including calls that did not connect (failedCall: true, with the reason and the SIP code). customerCharge is what the call cost you in USD.

Query parameterTypeAbout
limitinteger1 to 10000, default 50.
offsetintegerSkip this many calls, default 0.
Request
curl "https://secretvoip.com/portal-api/v1/voip/calls?limit=2" \
  -H "Authorization: Bearer $SVP_KEY"
Response 200
{
  "ok": true,
  "calls": [
    { "id": "21801", "callerId": "4570000000", "source": "office2", "destination": "4540911411",
      "startedAt": "2026-10-10T19:47:45.000Z", "durationSeconds": 533, "billedSeconds": 540,
      "customerCharge": 6.6564, "status": "answered", "sipId": 269, "routePrefix": "12345" },
    { "id": "F9", "callerId": "4570000000", "source": "office2", "destination": "4512345678",
      "startedAt": "2026-10-10T08:40:44.000Z", "durationSeconds": 0, "billedSeconds": 0,
      "customerCharge": 0, "status": "busy", "sipId": 269, "routePrefix": "99342",
      "failedCall": true, "cause": 2, "sipCode": 486 }
  ],
  "total": 2,
  "summary": { "billedSeconds": 540, "customerCharge": 6.6564 },
  "limit": 2,
  "offset": 0
}

Call rates

GET/voip/ratesscope: voip

The price per minute of every route and country (the same list as the Call rates page). multi lists routes that cover many countries.

Request
curl "https://secretvoip.com/portal-api/v1/voip/rates" \
  -H "Authorization: Bearer $SVP_KEY"
Response 200
{
  "ok": true,
  "entries": [
    { "routeId": "3737aeed-4124-442c-b262-d62495435e48", "countryId": "2974f8fc-2881-4f8e-9c8a-6611b71edc42",
      "routeName": "Denmark Standard V2", "countryName": "Denmark", "countryPrefix": "45",
      "price": "0.201800", "billingBlock": 30 }
  ],
  "multi": [
    { "routeId": "b697b640-20f5-4477-8e3f-5922a1750ac3", "routeName": "Worldwide",
      "countries": 191, "minPrice": "0.053300", "maxPrice": "3.069100" }
  ]
}

Top up SIP lines

POST/voip/topupscope: voip

Moves money from your main balance to your SIP balance, which calls are paid from. At least $10. 200 = done. 502 or 503 = the SIP lines could not be topped up and nothing was charged. 202 with "ok": false = the top-up is still being checked: do NOT send a new top-up - ask again with the SAME requestKey after a few minutes and you get the final result (state completed, or refunded with the money back on the main balance).

Body fieldTypeAbout
amountrequiredstringUS dollars, up to 4 decimals, for example "25.00".
requestKeyrequiredstringYour own id for this top-up (8 to 80 characters). The same key never moves money twice.
Request
curl -X POST "https://secretvoip.com/portal-api/v1/voip/topup" \
  -H "Authorization: Bearer $SVP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "25.00",
    "requestKey": "topup-2026-10-13"
  }'
Response 200
{
  "ok": true,
  "transfer": { "id": "…", "direction": "to_sip", "amount": "25.00", "state": "completed",
                "byAdmin": false, "createdAt": "2026-10-13T13:10:02.114Z",
                "completedAt": "2026-10-13T13:10:02.380Z", "error": null },
  "main": "63.6740",
  "sip": "150.0000"
}

Account

Balance

GET/account/balancescope: account

Your main balance (deposits, SMS) and SIP balance (calls), plus the last 50 movements of the main balance.

Request
curl "https://secretvoip.com/portal-api/v1/account/balance" \
  -H "Authorization: Bearer $SVP_KEY"
Response 200
{
  "ok": true,
  "main": "8.6740",
  "sip": "125.0000",
  "ledger": [
    { "id": 282, "kind": "sms", "label": "SMS", "amount": "-0.0752", "balanceAfter": "8.6740",
      "note": "SMS: 1 message(s)", "createdAt": "2026-10-13T13:07:38.447Z" }
  ],
  "canMove": true,
  "minTopUp": 10
}

Subscription

GET/account/subscriptionscope: account

Your SecretVoIP subscription: status, period and next renewal.

Request
curl "https://secretvoip.com/portal-api/v1/account/subscription" \
  -H "Authorization: Bearer $SVP_KEY"
Response 200
{
  "ok": true,
  "subscription": {
    "status": "active", "required": true, "planCode": "secretvoip_monthly_20", "priceUsd": 20,
    "autoRenew": true, "periodStart": "2026-10-01T09:00:00.000Z", "periodEnd": "2026-10-31T09:00:00.000Z",
    "nextRenewalAt": "2026-10-31T09:00:00.000Z", "currentBalanceUsd": 8.674, "sipBalanceUsd": 125,
    "memberCount": 1, …
  }
}

Usage

GET/account/usagescope: account

A short summary: balances, SIP lines and today's calls.

Request
curl "https://secretvoip.com/portal-api/v1/account/usage" \
  -H "Authorization: Bearer $SVP_KEY"
Response 200
{
  "ok": true,
  "account": { "username": "office", "balance": 125, "mainBalance": 8.674, "active": true, "plan": "Route 1" },
  "sip": { "total": 2, "active": 2, "registered": 1, "limit": 5 },
  "usage": { "callsToday": 0, "secondsToday": 0 }
}

Statuses

SMS messages

statusMeaning
queuedWaiting to be sent (or scheduled)
sendingBeing handed to the network
sentAccepted by the network, waiting for the delivery report
deliveredThe phone received it
failedNot delivered; reason says why, refunded says if the money came back
unknownThe result is being checked
cancelledA scheduled send that was cancelled, money back

reason of a failed message: route_unavailable, not_sent, sender_refused, bad_number, not_delivered, rejected, admin_refund, price_changed (the network price went up before it was sent: not sent, money back - send it again at the new price); checking while it is unknown. A send (batch.status) is scheduled, queued, sending, done or cancelled.

Calls

statusMeaning
answeredThe call connected and was billed
busyThe number was busy
no_answerNobody answered
cancelledThe caller hung up while it was ringing
failedThe call did not connect; sipCode tells why (for example 404, 486, 503)

Need help with the API? Write to our team on Telegram.