VoiceOne
REST · JSON · Webhooks

Klant-API

Mobiele abonnementen bestellen en beheren vanuit je eigen systeem

Met de klant-API bestelt en beheert je eigen software mobiele abonnementen met eSIM op je VoiceOne-account. Een nieuwe medewerker krijgt zo automatisch een zakelijk 06-nummer, een verloren telefoon blokkeer je meteen en een vertrokken medewerker zeg je op. Hoe het ervoor staat, hoor je via webhooks.

De API staat per account aan. Wil je hem gebruiken, mail dan [email protected]. Daarna maak je zelf sleutels aan in One, onder Mijn account → API. Daar beheer je ook je webhooks en zie je het logboek van alle aanroepen.

Op deze pagina

Snel starten

  1. Vraag via [email protected] of VoiceOne de sandbox voor je account aanzet.
  2. Maak in One een sandbox-sleutel aan (vo_test_…). Je ziet hem één keer, dus bewaar hem meteen veilig.
  3. Haal de producten op die je mag bestellen:
curl https://api.voiceone.nl/v1/products \
  -H "Authorization: Bearer vo_test_…"
  1. Bestel een abonnement. Geef bij elke POST een eigen Idempotency-Key mee:
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 de sandbox is het abonnement na ongeveer een minuut actief. Volg het met een webhook, of vraag het op met GET /v1/mobile-subscriptions/{id}.
  2. Werkt alles? Vraag dan live aan. Een live-sleutel (vo_live_…) werkt alleen vanaf de IP-adressen die je bij de sleutel opgeeft.

Basis

Adres, sleutel en formaat

  • Adres: https://api.voiceone.nl/v1. Alleen via https; http geeft 403.
  • Sleutel: stuur hem mee als Authorization: Bearer <sleutel>. Een sleutel begint met vo_live_ (live) of vo_test_ (sandbox), gevolgd door 40 letters en cijfers. De sleutel bepaalt de omgeving. VoiceOne bewaart alleen een hash: kwijt is een nieuwe aanmaken.
  • Formaat: JSON in UTF-8 met Content-Type: application/json, hooguit 64 kB per verzoek. Bedragen zijn strings met twee decimalen, in euro en exclusief btw. Tijden staan in ISO 8601 met tijdzone (Europe/Amsterdam), datums als YYYY-MM-DD en telefoonnummers in E.164 (+31612345678).
  • Request-id: elk antwoord heeft de header X-Request-Id. Noem die als je een vraag hebt over een aanroep.
  • Server naar server: de API kent geen CORS. Roep hem aan vanaf je server, nooit vanuit een browser of app: daar zou je sleutel voor iedereen leesbaar zijn.
  • Nieuwe velden en events kunnen erbij komen. Laat je systeem onbekende velden negeren.

Rechten per sleutel

Bij het aanmaken kies je welke rechten een sleutel krijgt. Geef een sleutel alleen wat je systeem nodig heeft.

subscriptions:read
Abonnementen en producten lezen.
subscriptions:write
Bestellen, wijzigen, blokkeren, deblokkeren, opzeggen, een nieuwe eSIM en een SIP-kanaal aanvragen. Hier zijn kosten aan verbonden.
esim:read
De activatiecode en bevestigingscode van een eSIM in het antwoord. Zonder dit recht zijn die velden null.
sip:read
De inloggegevens van een SIP-kanaal ophalen.
calls:read
Nog niet in gebruik (gesprekken, in een volgende versie).

Webhooks bevatten de eSIM-codes nooit, ook niet met esim:read. Haal ze op met GET /v1/mobile-subscriptions/{id} zodra esim.ready waar is.

Limieten

  • 120 aanroepen per minuut per sleutel. Daarboven 429 rate_limited met de header Retry-After (seconden).
  • Veel aanroepen met een ongeldige sleutel vanaf één IP-adres: 429 voor tien minuten.
  • Live alleen vanaf vaste IP-adressen: maximaal 20 adressen of reeksen per sleutel, IPv4 of IPv6, reeksen niet groter dan /16 (IPv4) of /32 (IPv6). De sandbox mag zonder.
  • Daglimiet op nieuwe abonnementen per account (standaard 10, af te spreken met VoiceOne). Daarboven wordt de bestelling gewoon aangenomen, met status awaiting_approval. VoiceOne zet hem na controle door.
  • Vervaldatum: een sleutel kan een einddatum hebben. Daarna geeft hij 401.

Idempotentie: nooit dubbel

  • Elke POST vraagt de header Idempotency-Key: 1 tot 200 tekens, letters, cijfers en . _ : -. Gebruik een nieuwe per handeling, bijvoorbeeld het id uit je eigen systeem.
  • Dezelfde Idempotency-Key met dezelfde body binnen 7 dagen geeft precies hetzelfde antwoord terug, met de header Idempotent-Replayed: true. Er wordt niets opnieuw uitgevoerd, dus na een time-out kun je veilig opnieuw proberen.
  • Dezelfde Idempotency-Key met een andere body, of vanaf een andere API-sleutel: 409 idempotency_conflict. Loopt het eerste verzoek nog: 409 request_in_progress met Retry-After.
  • Een 5xx, een 409 busy en een 429 worden niet bewaard: probeer het dan gerust opnieuw met dezelfde Idempotency-Key.
  • Is een bestelling eenmaal geplaatst, dan krijgt dat verzoek nooit meer een 5xx maar een 202.

Lijsten

GET /v1/mobile-subscriptions geeft de nieuwste eerst, standaard 50 per keer. Gebruik ?limit= (1–100) en voor de volgende pagina ?cursor= met de waarde van next_cursor. Filteren kan op ?status= en ?external_reference=. De lijst bevat alle mobiele abonnementen van je account, ook de abonnementen die niet via de API zijn besteld.

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

Fouten

Een fout heeft altijd deze vorm. details staat er alleen bij als er iets te melden is, zoals het veld dat niet klopt.

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

Laat je systeem reageren op code, niet op message: de tekst kan veranderen.

400 invalid_request
Geen geldige JSON, geen JSON-object, of een body groter dan 64 kB.
401 unauthorized
Geen sleutel, of een onbekende, ingetrokken of verlopen sleutel.
403 api_disabled
De API, of deze omgeving, staat voor je account uit.
403 ip_not_allowed
Het IP-adres staat niet bij de sleutel.
403 insufficient_scope
De sleutel mist het recht (details.required).
403 account_blocked
Je account is geblokkeerd, bijvoorbeeld door een betaalachterstand.
403 forbidden
Deze aanroep kan niet met deze sleutel, zoals sandbox/simulate met een live-sleutel.
403 webrtc_disabled
Bellen in de browser staat voor je account niet aan.
404 not_found
Onbekend endpoint, of een abonnement dat niet bestaat of bij een ander account hoort.
405 method_not_allowed
Verkeerde methode. De header Allow noemt de goede.
409 busy
Er loopt al een actie op deze simkaart of dit account. Probeer het zo opnieuw.
409 invalid_state
De actie past niet bij de stand, zoals deblokkeren terwijl hij niet geblokkeerd is. details geeft vaak de huidige status.
409 not_supported
Niet mogelijk voor dit product (een SIP-kanaal kan alleen bij Mobiel Compleet).
409 idempotency_conflict
Deze Idempotency-Key is al gebruikt voor een ander verzoek.
409 request_in_progress
Een verzoek met deze Idempotency-Key loopt nog.
422 validation_error
Ongeldige invoer. details.field noemt het veld.
422 product_not_allowed
Dit product is niet vrijgegeven voor je account (zie GET /v1/products).
422 idempotency_key_required
POST zonder Idempotency-Key.
429 rate_limited
Te veel aanroepen. Wacht het aantal seconden uit Retry-After.
500 internal
Onverwachte fout. Mail de request_id naar [email protected].
503 unavailable
Een onderdeel is tijdelijk niet beschikbaar. Probeer het later opnieuw.

Endpoints

Bij elke POST hoort een Idempotency-Key. Het {id} van een abonnement begint met ms_ (in de sandbox ms_test_).

GET/v1
Overzicht van de API, zonder sleutel.
GET/v1/products
Producten die je mag bestellen, met je prijs.subscriptions:read
GET/v1/mobile-subscriptions
Alle mobiele abonnementen van je account.subscriptions:read
POST/v1/mobile-subscriptions
Een nieuw abonnement bestellen.subscriptions:write
GET/v1/mobile-subscriptions/{id}
Eén abonnement, met de eSIM-codes (met esim:read).subscriptions:read
PATCH/v1/mobile-subscriptions/{id}
Naam, e-mailadres of kenmerk wijzigen.subscriptions:write
POST/v1/mobile-subscriptions/{id}/block
Bellen, sms en data meteen blokkeren.subscriptions:write
POST/v1/mobile-subscriptions/{id}/unblock
Een blokkade weer opheffen.subscriptions:write
POST/v1/mobile-subscriptions/{id}/cancel
Het abonnement opzeggen.subscriptions:write
POST/v1/mobile-subscriptions/{id}/esim-swap
Een nieuwe eSIM op hetzelfde nummer.subscriptions:write
POST/v1/mobile-subscriptions/{id}/sip-channel
Een SIP-kanaal aanvragen.subscriptions:write
GET/v1/mobile-subscriptions/{id}/sip-credentials
De inloggegevens van het SIP-kanaal.sip:read
POST/v1/mobile-subscriptions/{id}/webrtc-login
Een browserlogin maken (bellen in de browser).subscriptions:write
GET/v1/mobile-subscriptions/{id}/webrtc-login
De browserlogin met wachtwoord en verbindingsgegevens.sip:read
POST/v1/mobile-subscriptions/{id}/webrtc-login/reset-password
Een nieuw wachtwoord voor de browserlogin.subscriptions:write
POST/v1/mobile-subscriptions/{id}/webrtc-login/revoke
De browserlogin intrekken.subscriptions:write
POST/v1/sandbox/simulate
Alleen sandbox: een gebeurtenis nabootsen.vo_test_

Producten

{
  "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 de prijs die voor je account is afgesproken, anders de catalogusprijs. setup_fee_excl zijn de eenmalige kosten per nieuw abonnement (null als die er niet zijn).

Codes, namen en bedragen in de voorbeelden op deze pagina zijn verzonnen. De echte voor je account staan in het antwoord van GET /v1/products.

Het mobiele abonnement

Elk antwoord over een abonnement, en elke webhook, bevat dit 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 (boven de daglimiet, wacht op akkoord) · pending (wordt geleverd) · needs_attention (vraagt handwerk en duurt langer, maar is niet geweigerd) · active · blocked · ended · cancelled (bestelling geannuleerd vóór levering).
phone_number
Het 06-nummer zodra het bekend is (event subscription.number_assigned).
number
type is new of porting. Bij nummerbehoud staat in porting: current_number, status (requested, planned, completed of rejected) en planned_date.
sim
type is esim, of physical bij een fysieke simkaart die al op je account stond. status: to_order · ordered · shipped · active · blocked · ended.
esim
ready wordt waar zodra de eSIM klaarstaat. activation_code is de LPA-code (LPA:1$…) en confirmation_code de bevestigingscode; allebei alleen met esim:read en alleen in een antwoord, nooit in een webhook.
esim_swap
Na een aangevraagde nieuwe eSIM: status (ready, completed, revoked of failed), requested_at, completed_at en, met esim:read en alleen bij GET …/{id}, de codes van de nieuwe eSIM. Na completed staat de nieuwe eSIM in esim.
sip_channel
Na een aanvraag: status (pending, active of removed), username, registered en registered_at. De gebruikersnaam en de registratie alleen bij active; het wachtwoord staat hier nooit in.
webrtc_login
Bij een browserlogin: username, status (active) en online (zoals de gateway het meldt, of null). Anders null.
cancellation
Bij een opzegging: ends_at (de laatste dag dat het abonnement werkt) en stops_on (de dag dat het stopt).
external_reference
Je eigen kenmerk, bijvoorbeeld het id van de medewerker in je systeem. Komt terug in elk antwoord en elke webhook.
order
De bestelling waar het abonnement uit kwam, met id en number.

Bestellen

Bestel een nieuw abonnement met een nieuw nummer:

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" }
}

Of neem een bestaand 06-nummer mee (nummerbehoud):

{
  "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
Verplicht. Een code uit GET /v1/products.
user.first_name, user.last_name
Verplicht, 1–60 tekens. De naam komt op de factuurregel en in de verbruiksspecificatie.
user.email
Optioneel. Het adres waar de eSIM naartoe gaat; leeg is het hoofdadres van je account. Je systeem kan de eSIM ook zelf laten installeren met de activatiecode (esim:read).
external_reference
Optioneel, hooguit 100 tekens.
number.type
new (standaard) of porting. Bij porting is current_number verplicht (een Nederlands 06-nummer) en provider (de huidige aanbieder). Optioneel zijn customer_number en sim_number bij die aanbieder, en timing: contract_end (standaard) of asap. Met asap ga je akkoord met eventuele afkoopkosten bij de huidige aanbieder.
sim.type
Alleen esim in deze versie.
  • Antwoord: 202 met het abonnement, status pending of awaiting_approval.
  • VoiceOne stuurt bij een bestelling via de API geen orderbevestiging en geen oplevermail. Je systeem volgt de stand via webhooks.
  • Een SIP-kanaal vraag je na het bestellen apart aan (zie SIP-kanaal).

Beheren

Gegevens wijzigen

PATCH /v1/mobile-subscriptions/{id}

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

Alleen user.first_name, user.last_name, user.email en external_reference kunnen veranderen. Stuur alleen wat verandert. Antwoord 200 met het abonnement.

Blokkeren en deblokkeren

POST /v1/mobile-subscriptions/{id}/block
POST /v1/mobile-subscriptions/{id}/unblock
  • Blokkeert bellen, sms en data meteen, bijvoorbeeld bij verlies of als iemand uit dienst gaat. Geen body. Het kan tot ongeveer 90 seconden duren; antwoord 200 met het abonnement.
  • Al (niet) geblokkeerd, of geen actieve simkaart: 409 invalid_state. Loopt er al een actie: 409 busy. Deblokkeren kan alleen als je zelf blokkeerde, niet bij een blokkade door VoiceOne.

Opzeggen

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

{ "reason": "Employee left the company" }
  • Optioneel een reason (hooguit 500 tekens). Het abonnement stopt na een opzegtermijn van één maand, of aan het einde van een lopend contract als dat later is. Een mobiel abonnement stopt altijd per de 1e van een maand.
  • Antwoord 200 met het abonnement en cancellation, bijvoorbeeld { "ends_at": "2026-12-31", "stops_on": "2027-01-01" }. Alleen bij active of blocked. Een bestelling die nog geleverd wordt, zeg je op via [email protected].
  • Al opgezegd: 409 invalid_state. Account geblokkeerd: 403 account_blocked. VoiceOne mailt de gebruiker niet. Je krijgt de webhook subscription.cancellation_scheduled en op de einddatum subscription.ended.

Nieuwe eSIM

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

{
  "user": { "first_name": "Anna", "last_name": "de Vries", "email": "[email protected]" }
}
  • Een nieuwe eSIM op hetzelfde nummer, bijvoorbeeld bij een nieuwe telefoon of als het nummer naar een andere medewerker gaat. Geef dan optioneel de nieuwe user mee. De huidige eSIM blijft werken tot de nieuwe is geïnstalleerd.
  • Het kan tot ongeveer 90 seconden duren. Antwoord 200 met esim_swap.status: "ready" en, met esim:read, de codes van de nieuwe eSIM. Werd het verzoek verstuurd maar niet bevestigd, dan 202 met esim_swap.status: "failed": probeer het dan niet opnieuw, VoiceOne kijkt het na.
  • Alleen voor een actieve, niet-geblokkeerde eSIM. Staat er al een nieuwe klaar: 409. Hooguit een paar keer per 24 uur per simkaart, daarboven 429. Webhooks: subscription.esim_swap_ready en na de installatie subscription.esim_swap_completed.
  • Gaat het nummer naar een andere medewerker? Haal dan in One de toegang van de vorige medewerker weg (Mijn account → Gebruikers), anders kan die de codes van de nieuwe eSIM inzien.

SIP-kanaal

Bij Mobiel Compleet kan elke gebruiker een SIP-kanaal krijgen, zonder extra kosten. Daarmee meld je een SIP-client aan voor die gebruiker, bijvoorbeeld de dialer of softphone in je eigen systeem.

Aanvragen

  • POST /v1/mobile-subscriptions/{id}/sip-channel, zonder body en met een Idempotency-Key. Antwoord 202 met het abonnement en sip_channel.status: "pending".
  • VoiceOne maakt het kanaal binnen een paar minuten zodra de simkaart actief is. Dan volgt de webhook subscription.sip_channel_ready.
  • Is er al een kanaal, of loopt de aanvraag nog, dan 200 met het abonnement zoals het is: er komt nooit een tweede kanaal.
  • Geen Mobiel Compleet: 409 not_supported. Beëindigd: 409 invalid_state. Account geblokkeerd: 403 account_blocked. Hoort er nog een kanaal bij een vorige eigenaar van de simkaart: 409 invalid_state. VoiceOne ruimt het op; vraag het daarna opnieuw aan.

Inloggegevens

{
  "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, met het recht sip:read. Alleen als sip_channel.status active is, anders 404 met details.sip_channel_status.
  • Het wachtwoord staat alleen in dit antwoord, nooit in lijsten of webhooks, en het antwoord heeft Cache-Control: no-store. Bewaar het net zo zorgvuldig als je API-sleutel.
  • Heeft het abonnement een browserlogin, dan gebruikt onze gateway het kanaal: dan geeft dit endpoint 409 invalid_state. Eén kanaal kan niet tegelijk in de browser én in een eigen SIP-client.
  • port is null als VoiceOne geen vaste poort opgeeft: dan geldt de standaard van je SIP-client. extension kan null zijn als die nog niet bekend is. De registratie (registered, registered_at) leest VoiceOne hooguit eens per tien minuten uit; checked_at zegt wanneer.

Aanmelden

  1. Registreer je SIP-client met username en password op server.
  2. Bel vanaf die client één keer 901, toets de extensie van de gebruiker en de pincode (standaard 0000). De gebruiker is dan aangemeld op dit kanaal.
  3. Staat het IP-adres van de client op de IP-whitelist van je account, dan meldt de gebruiker zich voortaan vanzelf aan.

Bellen in de browser

Met onze WebRTC-gateway belt een gebruiker vanuit de browser, bijvoorbeeld in je CRM, via het SIP-kanaal van zijn Mobiel Compleet-abonnement. Je hebt geen eigen SIP-server of softphone nodig: de browser praat WebRTC met de gateway, en de gateway meldt het kanaal aan bij ons SIP-platform.

  • Bellen in de browser staat per account aan; vraag het aan via [email protected]. Tot die tijd geeft POST …/webrtc-login 403 webrtc_disabled.
  • Het abonnement is Mobiel Compleet en heeft een actief SIP-kanaal (POST …/sip-channel, daarna de webhook subscription.sip_channel_ready).
  • Eén browserlogin per abonnement. Dat kanaal gebruik je dan niet ook nog in een eigen SIP-client.

Zo werkt het

  1. POST /v1/mobile-subscriptions/{id}/webrtc-login, zonder body en met een Idempotency-Key. Antwoord 201 met de login (object: "webrtc_login"), zonder wachtwoord. Bestaat er al een, dan 200 met die login.
  2. GET /v1/mobile-subscriptions/{id}/webrtc-login met het recht sip:read: gebruikersnaam, wachtwoord, websocket_url en ice_servers (TURN met dezelfde login). Het antwoord heeft Cache-Control: no-store.
  3. Geef die gegevens via je eigen, ingelogde sessie aan de browser van de gebruiker en meld aan met een SIP-over-WebSocket-bibliotheek zoals JsSIP. ready is true zodra de gateway de login kent (binnen ongeveer een minuut).
  4. Nieuw wachtwoord: POST …/webrtc-login/reset-password; het oude werkt binnen een minuut niet meer. Intrekken: POST …/webrtc-login/revoke; lopende gesprekken worden dan verbroken.
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"
}

Voorbeeld met 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 });

Het wachtwoord komt in de browser van de gebruiker: geef elke gebruiker zijn eigen login, bewaar het niet in localStorage, en trek de login in als iemand vertrekt.

Wat je kunt bellen

  • Nederlandse nummers, 0800, 14-nummers, interne nummers (2–4 cijfers), 112 en 901 (aanmelden met extensie en pincode). Buitenland en betaalnummers (0900, 0906, 0909, 18xx) alleen als VoiceOne dat voor de login aanzet.
  • Standaardlimieten per login: 2 gesprekken tegelijk, 120 uitgaande gesprekken per uur en 2 uur per gesprek. VoiceOne kan ze per login aanpassen; ze staan in limits.
  • Inkomende gesprekken op het nummer gaan in de browser over zolang de gebruiker is aangemeld.
  • Is het abonnement geblokkeerd of beëindigd, is het account geblokkeerd, of staat de API voor je account uit, dan werkt de login binnen een minuut niet meer.

Weigeringen (SIP-status)

401
Onbekende login of fout wachtwoord bij het aanmelden.
403
Dit nummer mag deze login niet bellen (buitenland, betaalnummer of een ander domein).
404
Geen geldig nummer.
486
Limiet van de login bereikt (tegelijk of per uur).
503
De gateway is tijdelijk vol; probeer het zo opnieuw.

Webhooks

Stel in One per omgeving één of meer adressen in en kies de events (niets gekozen is alles). Elke wijziging vraagt tweestapsverificatie. Elk adres krijgt een eigen geheim (whsec_…), dat je in One kunt inzien en vernieuwen. Een melding ontstaat binnen ongeveer een minuut na de wijziging.

Het adres moet met https:// beginnen, een domeinnaam hebben (geen IP-adres), van buitenaf bereikbaar zijn en poort 443 of 8443 gebruiken.

{
  "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
Het id van de melding. Ontdubbel hierop: een melding kan vaker binnenkomen, bijvoorbeeld na een time-out.
VoiceOne-Event-Type
Het event, zoals subscription.active.
VoiceOne-Signature
t=<unix-tijd>,v1=<hex>, met v1 = HMAC-SHA256(geheim, "<t>.<ruwe body>").

Handtekening controleren

Controleer elke melding vóór je hem verwerkt: bereken de HMAC over de ruwe body precies zoals die binnenkwam, vergelijk in constante tijd met v1 en wijs af als t meer dan vijf minuten afwijkt.

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;
}

Aflevering

  • Een 2xx binnen 10 seconden is gelukt. Doorverwijzingen worden niet gevolgd.
  • Anders probeert VoiceOne het opnieuw na 1 minuut, 5 minuten, 30 minuten, 2 uur en 12 uur (zes pogingen). Daarna stopt het; in One zie je dat en kun je de melding opnieuw versturen.
  • De volgorde is niet gegarandeerd. Gebruik created_at en het object in de melding als de actuele stand.
  • Meldingen blijven 30 dagen bewaard. Met de testknop in One krijg je een melding van het type test.ping.

Events

subscription.created
Nieuw abonnement aangenomen.
subscription.awaiting_approval
Boven de daglimiet: wacht op akkoord van VoiceOne.
subscription.needs_attention
Vraagt handwerk en duurt langer, maar is niet geweigerd.
subscription.active
Het abonnement werkt.
subscription.esim_ready
De eSIM staat klaar. Haal de codes op met GET …/{id} (met esim:read).
subscription.number_assigned
Het 06-nummer is bekend.
subscription.blocked
Geblokkeerd.
subscription.unblocked
De blokkade is opgeheven.
subscription.cancellation_scheduled
Opgezegd. cancellation noemt de datums.
subscription.ended
Het abonnement is gestopt.
subscription.cancelled
De bestelling is vóór levering geannuleerd.
subscription.esim_swap_ready
Een nieuwe eSIM staat klaar om te installeren.
subscription.esim_swap_completed
De nieuwe eSIM is geïnstalleerd.
subscription.sip_channel_ready
Het SIP-kanaal is klaar. Haal de gegevens op met sip-credentials.
porting.planned
Nummerbehoud is ingepland, of de datum is verzet (number.porting.planned_date).
porting.completed
Het nummer is overgezet.
porting.rejected
Nummerbehoud is afgewezen. VoiceOne zoekt uit wat er nodig is.

Sandbox

  • Zelfde adres, endpoints en antwoorden, met een vo_test_-sleutel. environment is dan "sandbox".
  • Niets is echt: er ontstaan geen bestellingen, facturen of mails. Ids beginnen met ms_test_, nummers met +316000 (bel ze niet) en de activatiecode is LPA:1$sandbox.voiceone.nl$TEST-….
  • Een nieuw abonnement is na ongeveer een minuut vanzelf active. Een SIP-kanaal ook, ongeveer een minuut na de aanvraag, als het abonnement actief is.
  • GET …/sip-credentials geeft in de sandbox voor elk abonnement hetzelfde echte testkanaal van VoiceOne (shared_test_channel: true), zodat je het aanmelden echt kunt proberen. Is dat nog niet ingesteld, dan 503 unavailable.
  • Een browserlogin in de sandbox is nagebootst (simulated: true, ready: false): hij werkt niet op de gateway. Echt bellen in de browser kan alleen live.

Gebeurtenissen nabootsen

Met POST /v1/sandbox/simulate (met Idempotency-Key) zet je een sandbox-abonnement in een stand en verstuur je de bijbehorende webhook. Mogelijke 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 werkt na een esim-swap, subscription.ended na een cancel en subscription.sip_channel_ready na een sip-channel op een actief abonnement.

Hulp

Vragen over de API, of werkt iets niet zoals hier staat? Mail [email protected] en noem de request_id uit het antwoord.

Ondersteuning voor de API en andere ontwikkelvragen gaat alleen per e-mail via dit adres, niet via de gewone klantenservice, de chat of de telefoon.

Storingen en onderhoud staan op voiceone.nl/status.

Open data zonder sleutel (prijzen, domeinnamen, storingen) vind je bij Voor ontwikkelaars.

Mmm... cookies! 🍪

Nee, niet die lekkere met chocola (helaas!), maar wel bijna net zo belangrijk! We gebruiken cookies om je de beste ervaring te geven en onze website te verbeteren.