VoiceOne
REST · JSON · Webhooks

Customer API

Order and manage mobile subscriptions from your own system

With the customer API, your own software orders and manages mobile subscriptions with eSIM on your VoiceOne account. A new employee automatically gets a business mobile number, a lost phone is blocked right away and a leaver is cancelled. Webhooks tell you how things are going.

The API is enabled per account. To use it, email [email protected]. You then create keys yourself in One, under My account → API. That is also where you manage your webhooks and see the log of every request.

On this page

Quick start

  1. Email [email protected] to have the sandbox enabled for your account.
  2. Create a sandbox key in One (vo_test_…). It is shown once, so store it safely straight away.
  3. Fetch the products you are allowed to order:
curl https://api.voiceone.nl/v1/products \
  -H "Authorization: Bearer vo_test_…"
  1. Order a subscription. Send a unique Idempotency-Key with every POST:
curl https://api.voiceone.nl/v1/mobile-subscriptions \
  -H "Authorization: Bearer vo_test_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: new-advisor-42" \
  -d '{
    "product": "your_product_code",
    "user": { "first_name": "Jan", "last_name": "Jansen", "email": "[email protected]" },
    "external_reference": "advisor-42"
  }'
  1. In the sandbox, the subscription becomes active after about a minute. Follow it with a webhook, or fetch it with GET /v1/mobile-subscriptions/{id}.
  2. Everything working? Then ask for live access. A live key (vo_live_…) only works from the IP addresses you set on the key.

Basics

Address, key and format

  • Address: https://api.voiceone.nl/v1. HTTPS only; plain http returns 403.
  • Key: send it as Authorization: Bearer <key>. A key starts with vo_live_ (live) or vo_test_ (sandbox), followed by 40 letters and digits. The key decides the environment. VoiceOne only stores a hash: a lost key means creating a new one.
  • Format: JSON in UTF-8 with Content-Type: application/json, at most 64 kB per request. Amounts are strings with two decimals, in euros, excluding VAT. Times are ISO 8601 with a time zone (Europe/Amsterdam), dates are YYYY-MM-DD and phone numbers are E.164 (+31612345678).
  • Request ID: every response has an X-Request-Id header. Include it when you have a question about a request.
  • Server to server: the API has no CORS. Call it from your server, never from a browser or app, where your key would be readable by anyone.
  • New fields and events may be added. Make your system ignore fields it does not know.

Permissions per key

You choose the permissions of a key when you create it. Give a key only what your system needs.

subscriptions:read
Read subscriptions and products.
subscriptions:write
Order, change, block, unblock, cancel, and request a new eSIM or a SIP channel. These actions cost money.
esim:read
The activation code and confirmation code of an eSIM in the response. Without this permission those fields are null.
sip:read
Fetch the credentials of a SIP channel.
calls:read
Not in use yet (calls, in a future version).

Webhooks never contain the eSIM codes, not even with esim:read. Fetch them with GET /v1/mobile-subscriptions/{id} once esim.ready is true.

Limits

  • 120 requests per minute per key. Above that, 429 rate_limited with a Retry-After header (seconds).
  • Many requests with an invalid key from one IP address: 429 for ten minutes.
  • Live only from fixed IP addresses: at most 20 addresses or ranges per key, IPv4 or IPv6, ranges no larger than /16 (IPv4) or /32 (IPv6). The sandbox works without.
  • Daily limit on new subscriptions per account (10 by default, agreed with VoiceOne). Above it, the order is still accepted, with status awaiting_approval. VoiceOne releases it after a check.
  • Expiry date: a key can have an end date. After it, the key returns 401.

Idempotency: never twice

  • Every POST needs an Idempotency-Key header: 1 to 200 characters, letters, digits and . _ : -. Use a new one per action, such as the ID from your own system.
  • The same Idempotency-Key with the same body within 7 days returns exactly the same response, with the header Idempotent-Replayed: true. Nothing is executed again, so you can safely retry after a timeout.
  • The same Idempotency-Key with a different body, or from a different API key: 409 idempotency_conflict. Is the first request still running: 409 request_in_progress with Retry-After.
  • A 5xx, a 409 busy and a 429 are not stored: feel free to retry with the same Idempotency-Key.
  • Once an order has been placed, that request never gets a 5xx again, but a 202.

Lists

GET /v1/mobile-subscriptions returns the newest first, 50 at a time by default. Use ?limit= (1–100) and, for the next page, ?cursor= with the value of next_cursor. You can filter on ?status= and ?external_reference=. The list contains every mobile subscription on your account, including the ones not ordered through the API.

{
  "data": [ { "object": "mobile_subscription", "id": "ms_1234", … } ],
  "has_more": true,
  "next_cursor": "ms_1234"
}

Errors

An error always has this shape. details is only present when there is something to report, such as the field that is wrong.

{
  "error": {
    "code": "validation_error",
    "message": "user.last_name is required (1-60 characters).",
    "details": { "field": "user.last_name" },
    "request_id": "HrRbWgZ9J6Erbn70"
  }
}

Make your system act on code, not on message: the text may change.

400 invalid_request
Invalid JSON, not a JSON object, or a body larger than 64 kB.
401 unauthorized
No key, or an unknown, revoked or expired key.
403 api_disabled
The API, or this environment, is disabled for your account.
403 ip_not_allowed
The IP address is not allowed for the key.
403 insufficient_scope
The key lacks the permission (details.required).
403 account_blocked
Your account is blocked, for example because of overdue payments.
403 forbidden
This request is not possible with this key, such as sandbox/simulate with a live key.
403 webrtc_disabled
Calling in the browser is not enabled for your account.
404 not_found
Unknown endpoint, or a subscription that does not exist or belongs to another account.
405 method_not_allowed
Wrong method. The Allow header names the right one.
409 busy
An action is already running on this SIM or account. Try again shortly.
409 invalid_state
The action does not fit the current state, such as unblocking a SIM that is not blocked. details often gives the current status.
409 not_supported
Not possible for this product (a SIP channel is only available for Mobiel Compleet).
409 idempotency_conflict
This Idempotency-Key was already used for a different request.
409 request_in_progress
A request with this Idempotency-Key is still running.
422 validation_error
Invalid input. details.field names the field.
422 product_not_allowed
This product is not enabled for your account (see GET /v1/products).
422 idempotency_key_required
POST without an Idempotency-Key.
429 rate_limited
Too many requests. Wait the number of seconds in Retry-After.
500 internal
Unexpected error. Email the request_id to [email protected].
503 unavailable
A component is temporarily unavailable. Try again later.

Endpoints

Every POST needs an Idempotency-Key. The {id} of a subscription starts with ms_ (ms_test_ in the sandbox).

GET/v1
API overview, no key needed.
GET/v1/products
Products you may order, with your price.subscriptions:read
GET/v1/mobile-subscriptions
Every mobile subscription on your account.subscriptions:read
POST/v1/mobile-subscriptions
Order a new subscription.subscriptions:write
GET/v1/mobile-subscriptions/{id}
One subscription, including the eSIM codes (with esim:read).subscriptions:read
PATCH/v1/mobile-subscriptions/{id}
Change the name, email address or reference.subscriptions:write
POST/v1/mobile-subscriptions/{id}/block
Block calls, SMS and data right away.subscriptions:write
POST/v1/mobile-subscriptions/{id}/unblock
Lift a block.subscriptions:write
POST/v1/mobile-subscriptions/{id}/cancel
Cancel the subscription.subscriptions:write
POST/v1/mobile-subscriptions/{id}/esim-swap
A new eSIM on the same number.subscriptions:write
POST/v1/mobile-subscriptions/{id}/sip-channel
Request a SIP channel.subscriptions:write
GET/v1/mobile-subscriptions/{id}/sip-credentials
The credentials of the SIP channel.sip:read
POST/v1/mobile-subscriptions/{id}/webrtc-login
Create a browser login (calling in the browser).subscriptions:write
GET/v1/mobile-subscriptions/{id}/webrtc-login
The browser login with password and connection details.sip:read
POST/v1/mobile-subscriptions/{id}/webrtc-login/reset-password
A new password for the browser login.subscriptions:write
POST/v1/mobile-subscriptions/{id}/webrtc-login/revoke
Revoke the browser login.subscriptions:write
POST/v1/sandbox/simulate
Sandbox only: simulate an event.vo_test_

Products

{
  "data": [
    {
      "object": "product",
      "code": "your_product_code",
      "name": "Mobiel Compleet",
      "type": "mobile_subscription",
      "price_excl": "x.xx",
      "period": "month",
      "setup_fee_excl": "x.xx",
      "currency": "EUR"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

price_excl is the price agreed for your account, otherwise the list price. setup_fee_excl is the one-off fee per new subscription (null if there is none).

Codes, names and amounts in the examples on this page are made up. The real ones for your account are in the response of GET /v1/products.

The mobile subscription

Every response about a subscription, and every webhook, contains this object.

{
  "object": "mobile_subscription",
  "id": "ms_1234",
  "environment": "live",
  "status": "pending",
  "product": { "code": "your_product_code", "name": "Mobiel Compleet" },
  "user": { "first_name": "Jan", "last_name": "Jansen", "email": "[email protected]" },
  "external_reference": "advisor-42",
  "phone_number": null,
  "number": { "type": "new", "porting": null },
  "sim": { "type": "esim", "status": "to_order" },
  "esim": { "ready": false, "activation_code": null, "confirmation_code": null },
  "esim_swap": null,
  "sip_channel": null,
  "order": { "id": "ord_567", "number": "ORD20261187" },
  "cancellation": null,
  "created_at": "2026-11-03T14:02:11+01:00",
  "activated_at": null
}
status
awaiting_approval (above the daily limit, waiting for approval) · pending (being delivered) · needs_attention (needs manual work and takes longer, but is not rejected) · active · blocked · ended · cancelled (order cancelled before delivery).
phone_number
The mobile number once it is known (event subscription.number_assigned).
number
type is new or porting. When keeping a number, porting holds current_number, status (requested, planned, completed or rejected) and planned_date.
sim
type is esim, or physical for a physical SIM that was already on your account. status: to_order · ordered · shipped · active · blocked · ended.
esim
ready becomes true once the eSIM is available. activation_code is the LPA code (LPA:1$…) and confirmation_code the confirmation code; both only with esim:read and only in a response, never in a webhook.
esim_swap
After a new eSIM was requested: status (ready, completed, revoked or failed), requested_at, completed_at and, with esim:read and only for GET …/{id}, the codes of the new eSIM. After completed, the new eSIM is in esim.
sip_channel
After a request: status (pending, active or removed), username, registered and registered_at. The username and registration only when active; the password is never in here.
webrtc_login
With a browser login: username, status (active) and online (as reported by the gateway, or null). Otherwise null.
cancellation
After a cancellation: ends_at (the last day the subscription works) and stops_on (the day it stops).
external_reference
Your own reference, such as the employee ID in your system. Returned in every response and webhook.
order
The order the subscription came from, with id and number.

Ordering

Order a new subscription with a new number:

POST /v1/mobile-subscriptions

{
  "product": "your_product_code",
  "user": { "first_name": "Jan", "last_name": "Jansen", "email": "[email protected]" },
  "external_reference": "advisor-42",
  "number": { "type": "new" },
  "sim": { "type": "esim" }
}

Or keep an existing Dutch mobile number (number porting):

{
  "product": "your_product_code",
  "user": { "first_name": "Jan", "last_name": "Jansen" },
  "number": {
    "type": "porting",
    "current_number": "+31612345678",
    "provider": "Example Mobile",
    "customer_number": "12345678",
    "timing": "contract_end"
  }
}
product
Required. A code from GET /v1/products.
user.first_name, user.last_name
Required, 1–60 characters. The name appears on the invoice line and in the usage statement.
user.email
Optional. The address the eSIM is sent to; empty means the main address of your account. Your system can also install the eSIM itself using the activation code (esim:read).
external_reference
Optional, at most 100 characters.
number.type
new (default) or porting. With porting, current_number (a Dutch mobile number starting with 06) and provider (the current provider) are required. Optional are customer_number and sim_number at that provider, and timing: contract_end (default) or asap. With asap you accept any early termination fee at the current provider.
sim.type
Only esim in this version.
  • Response: 202 with the subscription, status pending or awaiting_approval.
  • For orders through the API, VoiceOne sends no order confirmation and no delivery email. Your system follows the state through webhooks.
  • You request a SIP channel separately after ordering (see SIP channel).

Managing

Changing details

PATCH /v1/mobile-subscriptions/{id}

{
  "user": { "first_name": "Anna", "last_name": "de Vries", "email": "[email protected]" },
  "external_reference": "advisor-57"
}

Only user.first_name, user.last_name, user.email and external_reference can change. Send only what changes. Response 200 with the subscription.

Blocking and unblocking

POST /v1/mobile-subscriptions/{id}/block
POST /v1/mobile-subscriptions/{id}/unblock
  • Blocks calls, SMS and data right away, for example after a loss or when someone leaves. No body. It can take up to about 90 seconds; response 200 with the subscription.
  • Already (un)blocked, or not an active SIM: 409 invalid_state. Another action running: 409 busy. Unblocking is only possible if you blocked it yourself, not after a block by VoiceOne.

Cancelling

POST /v1/mobile-subscriptions/{id}/cancel

{ "reason": "Employee left the company" }
  • Optionally a reason (at most 500 characters). The subscription stops after a notice period of one month, or at the end of a running contract if that is later. A mobile subscription always stops on the 1st of a month.
  • Response 200 with the subscription and cancellation, for example { "ends_at": "2026-12-31", "stops_on": "2027-01-01" }. Only for active or blocked. To cancel an order that is still being delivered, email [email protected].
  • Already cancelled: 409 invalid_state. Account blocked: 403 account_blocked. VoiceOne does not email the user. You receive the webhook subscription.cancellation_scheduled, and subscription.ended on the end date.

New eSIM

POST /v1/mobile-subscriptions/{id}/esim-swap

{
  "user": { "first_name": "Anna", "last_name": "de Vries", "email": "[email protected]" }
}
  • A new eSIM on the same number, for example for a new phone or when the number moves to another employee. In that case, optionally send the new user. The current eSIM keeps working until the new one is installed.
  • It can take up to about 90 seconds. Response 200 with esim_swap.status: "ready" and, with esim:read, the codes of the new eSIM. If the request was sent but not confirmed, the response is 202 with esim_swap.status: "failed": do not retry, VoiceOne will look into it.
  • Only for an active, unblocked eSIM. A new one already waiting: 409. At most a few times per 24 hours per SIM, above that 429. Webhooks: subscription.esim_swap_ready and, after installation, subscription.esim_swap_completed.
  • Number moving to another employee? Remove the previous employee’s access in One (My account → Users), otherwise they can see the codes of the new eSIM.

SIP channel

With Mobiel Compleet, every user can get a SIP channel at no extra cost. It lets you register a SIP client for that user, such as the dialer or softphone in your own system.

Requesting

  • POST /v1/mobile-subscriptions/{id}/sip-channel, without a body and with an Idempotency-Key. Response 202 with the subscription and sip_channel.status: "pending".
  • VoiceOne creates the channel within a few minutes once the SIM is active. The webhook subscription.sip_channel_ready follows.
  • If a channel already exists or the request is still running, the response is 200 with the subscription as it is: there is never a second channel.
  • Not Mobiel Compleet: 409 not_supported. Ended: 409 invalid_state. Account blocked: 403 account_blocked. A channel still belongs to a previous owner of the SIM: 409 invalid_state. VoiceOne cleans it up; request it again afterwards.

Credentials

{
  "object": "sip_credentials",
  "subscription_id": "ms_1234",
  "environment": "live",
  "server": "mv.voiceone.nl",
  "port": null,
  "username": "example7k2q",
  "password": "…",
  "extension": "301",
  "registration": {
    "registered": true,
    "registered_at": "2026-11-03T10:00:00+01:00",
    "checked_at": "2026-11-03T10:05:12+01:00"
  },
  "instructions": "Register a SIP client with this username and password on the server. Then dial 901 …"
}
  • GET /v1/mobile-subscriptions/{id}/sip-credentials, with the sip:read permission. Only when sip_channel.status is active, otherwise 404 with details.sip_channel_status.
  • The password is only in this response, never in lists or webhooks, and the response has Cache-Control: no-store. Store it as carefully as your API key.
  • If the subscription has a browser login, our gateway uses the channel: this endpoint then returns 409 invalid_state. A channel cannot be used in the browser and in your own SIP client at the same time.
  • port is null when VoiceOne sets no fixed port: your SIP client’s default applies. extension can be null while it is not known yet. VoiceOne reads the registration (registered, registered_at) at most once every ten minutes; checked_at says when.

Logging in

  1. Register your SIP client with username and password on server.
  2. From that client, dial 901 once, then enter the user’s extension and the PIN (0000 by default). The user is then logged in on this channel.
  3. If the client’s IP address is on your account’s IP whitelist, the user is logged in automatically from then on.

Calling in the browser

With our WebRTC gateway, a user calls from the browser, for example in your CRM, over the SIP channel of their Mobiel Compleet subscription. You need no SIP server or softphone of your own: the browser speaks WebRTC to the gateway, and the gateway registers the channel with our SIP platform.

  • Calling in the browser is enabled per account; request it at [email protected]. Until then, POST …/webrtc-login returns 403 webrtc_disabled.
  • The subscription is Mobiel Compleet and has an active SIP channel (POST …/sip-channel, then the webhook subscription.sip_channel_ready).
  • One browser login per subscription. Do not also use that channel in your own SIP client.

How it works

  1. POST /v1/mobile-subscriptions/{id}/webrtc-login, without a body and with an Idempotency-Key. Response 201 with the login (object: "webrtc_login"), without the password. If one already exists, 200 with that login.
  2. GET /v1/mobile-subscriptions/{id}/webrtc-login with the sip:read permission: username, password, websocket_url and ice_servers (TURN with the same login). The response has Cache-Control: no-store.
  3. Hand these details to the user’s browser over your own signed-in session and register with a SIP-over-WebSocket library such as JsSIP. ready is true once the gateway knows the login (within about a minute).
  4. New password: POST …/webrtc-login/reset-password; the old one stops working within a minute. Revoke: POST …/webrtc-login/revoke; calls in progress are disconnected.
GET /v1/mobile-subscriptions/{id}/webrtc-login

{
  "object": "webrtc_credentials",
  "subscription_id": "ms_1234",
  "environment": "live",
  "username": "123456-k3x9qa",
  "password": "…",
  "sip_uri": "sip:[email protected]",
  "websocket_url": "wss://webrtc.voiceone.nl/ws",
  "ice_servers": [
    {
      "urls": ["turns:turn.voiceone.nl:443?transport=tcp", "turn:turn.voiceone.nl:3478"],
      "username": "123456-k3x9qa",
      "credential": "…"
    }
  ],
  "ready": true,
  "limits": { "max_concurrent": 2, "max_calls_per_hour": 120, "max_call_seconds": 7200,
              "allow_international": false, "allow_premium": false },
  "online": true,
  "online_at": "2026-11-03T14:05:03+01:00"
}

Example with JsSIP

// login = the response of GET /v1/mobile-subscriptions/{id}/webrtc-login,
// fetched by your server and handed to the signed-in user's browser.
const ua = new JsSIP.UA({
  sockets: [new JsSIP.WebSocketInterface(login.websocket_url)],
  uri: login.sip_uri,
  password: login.password,
  register: true,
  session_timers: false,
});
const pcConfig = { iceServers: login.ice_servers };

ua.on('newRTCSession', (e) => {
  if (e.originator === 'remote') {
    e.session.answer({ mediaConstraints: { audio: true, video: false }, pcConfig });
  }
});
ua.start();

// Call out:
const session = ua.call('0612345678', { mediaConstraints: { audio: true, video: false }, pcConfig });

The password ends up in the user’s browser: give every user their own login, do not keep it in localStorage, and revoke the login when someone leaves.

What you can call

  • Dutch numbers, 0800, 14 numbers, internal numbers (2–4 digits), 112 and 901 (log in with extension and PIN). International and premium numbers (0900, 0906, 0909, 18xx) only if VoiceOne enables them for the login.
  • Default limits per login: 2 calls at a time, 120 outgoing calls per hour and 2 hours per call. VoiceOne can adjust them per login; they are in limits.
  • Incoming calls to the number ring in the browser while the user is registered.
  • If the subscription is blocked or has ended, the account is blocked, or the API is disabled for your account, the login stops working within a minute.

Rejections (SIP status)

401
Unknown login or wrong password when registering.
403
This login may not call this number (international, premium or another domain).
404
Not a valid number.
486
Login limit reached (at a time or per hour).
503
The gateway is temporarily full; try again shortly.

Webhooks

In One, set one or more addresses per environment and choose the events (none selected means all). Every change requires two-factor verification. Each address gets its own secret (whsec_…), which you can view and renew in One. An event is created within about a minute of the change.

The address must start with https://, use a domain name (not an IP address), be reachable from the internet and use port 443 or 8443.

{
  "id": "evt_9c2f0a1b7d4e8c3a5f6b2d1e",
  "object": "event",
  "type": "subscription.active",
  "environment": "live",
  "created_at": "2026-11-03T14:05:40+01:00",
  "data": { "object": { "object": "mobile_subscription", "id": "ms_1234", … } }
}

Headers

VoiceOne-Event-Id
The id of the event. Deduplicate on it: an event can arrive more than once, for example after a timeout.
VoiceOne-Event-Type
The event type, such as subscription.active.
VoiceOne-Signature
t=<unix time>,v1=<hex>, where v1 = HMAC-SHA256(secret, "<t>.<raw body>").

Verifying the signature

Verify every event before you process it: compute the HMAC over the raw body exactly as received, compare it with v1 in constant time, and reject it if t is more than five minutes off.

import crypto from 'node:crypto';

// rawBody: the request body exactly as received (a string, not re-serialised JSON).
export function verifyVoiceOne(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest();
  const given = Buffer.from(parts.v1 || '', 'hex');
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
<?php
function verifyVoiceOne(string $rawBody, string $signatureHeader, string $secret): bool {
    parse_str(str_replace(',', '&', $signatureHeader), $parts);
    $t = (int)($parts['t'] ?? 0);
    if ($t === 0 || abs(time() - $t) > 300) return false;
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    return hash_equals($expected, (string)($parts['v1'] ?? ''));
}

$raw = file_get_contents('php://input');
if (!verifyVoiceOne($raw, $_SERVER['HTTP_VOICEONE_SIGNATURE'] ?? '', (string)getenv('VOICEONE_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}

Delivery

  • A 2xx within 10 seconds counts as delivered. Redirects are not followed.
  • Otherwise VoiceOne retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours (six attempts). After that it stops; One shows this and lets you resend the event.
  • Order is not guaranteed. Use created_at and the object in the event as the current state.
  • Events are kept for 30 days. The test button in One sends an event of type test.ping.

Events

subscription.created
New subscription accepted.
subscription.awaiting_approval
Above the daily limit: waiting for approval by VoiceOne.
subscription.needs_attention
Needs manual work and takes longer, but is not rejected.
subscription.active
The subscription works.
subscription.esim_ready
The eSIM is available. Fetch the codes with GET …/{id} (with esim:read).
subscription.number_assigned
The mobile number is known.
subscription.blocked
Blocked.
subscription.unblocked
The block was lifted.
subscription.cancellation_scheduled
Cancelled. cancellation holds the dates.
subscription.ended
The subscription has stopped.
subscription.cancelled
The order was cancelled before delivery.
subscription.esim_swap_ready
A new eSIM is ready to be installed.
subscription.esim_swap_completed
The new eSIM has been installed.
subscription.sip_channel_ready
The SIP channel is ready. Fetch the details with sip-credentials.
porting.planned
Number porting is scheduled, or the date moved (number.porting.planned_date).
porting.completed
The number has been transferred.
porting.rejected
Number porting was rejected. VoiceOne will work out what is needed.

Sandbox

  • Same address, endpoints and responses, with a vo_test_ key. environment is then "sandbox".
  • Nothing is real: no orders, invoices or emails are created. IDs start with ms_test_, numbers with +316000 (do not call them) and the activation code is LPA:1$sandbox.voiceone.nl$TEST-….
  • A new subscription becomes active by itself after about a minute. So does a SIP channel, about a minute after the request, if the subscription is active.
  • In the sandbox, GET …/sip-credentials returns the same real VoiceOne test channel for every subscription (shared_test_channel: true), so you can really try registering. If it has not been set up yet: 503 unavailable.
  • A browser login in the sandbox is simulated (simulated: true, ready: false): it does not work on the gateway. Real calling in the browser is live only.

Simulating events

With POST /v1/sandbox/simulate (with an Idempotency-Key) you put a sandbox subscription in a state and send the matching webhook. Possible events:

  • subscription.active
  • subscription.needs_attention
  • subscription.blocked
  • subscription.unblocked
  • porting.planned
  • porting.completed
  • subscription.esim_swap_completed
  • subscription.ended
  • subscription.sip_channel_ready
POST /v1/sandbox/simulate

{ "event": "subscription.active", "subscription": "ms_test_8f2k1a" }

subscription.esim_swap_completed works after an esim-swap, subscription.ended after a cancel, and subscription.sip_channel_ready after a sip-channel on an active subscription.

Support

Questions about the API, or does something not work as described here? Email [email protected] and include the request_id from the response.

Support for the API and other development questions is by email only, at this address. Not through regular customer service, chat or phone.

Outages and maintenance are listed on voiceone.nl/status.

Open data without a key (prices, domain names, outages) is at For developers.

Mmm... cookies! 🍪

Not the chocolate chip kind (sadly!), but almost as important. We use cookies to give you the best experience and to improve our website.