WiseParts
Calls

Post a call event

Creates a call, or advances one that already exists.
POSTGET
https://app.wiseparts.ai/api/v1/voice/call

Creates a call, or advances one that already exists.

Send one request per state change on the same call, reusing the same id for its whole lifetime — WiseParts matches on that value. The match is scoped to the integration the token belongs to, not to the whole account, so two phone systems on the same account can use overlapping identifiers without colliding.

GET is accepted with the same parameters in the query string, for phone systems that can only fire a URL. It is not a read: both verbs create and advance calls.

Two fields you may see named in an error, source_id and user_id, are filled in by WiseParts — the first from the integration's configuration, the second from the extension-to-user mapping. Sending them yourself has no effect.

A call only ever moves forward

Each status carries a weight, and an event is written only when its weight is at least the weight of the status already on the call. Statuses of equal weight replace one another freely, so no-answer after canceled is accepted, while ringing after answered is not. Equal weight includes a status repeating itself: a second completed on a completed call passes the gate exactly like the first one did.

The part worth designing around: when an event is rejected, nothing on the call is updated — not merely the status. A late ringing that also carried end_time, duration or caller_id loses all of them. Put the values you care about on an event whose status is allowed to land.

A rejected event still answers 200, and so does an event a handler decides is not relevant and skips outright. A success therefore confirms receipt, not that anything was written.

Call statuses

StatusWeightMeaningEnds the call?
queued10Accepted into a queue, not yet offered to an agent. The one status that creates or advances a call without raising a realtime notification, so a call that only ever reports queued reaches reports and activity but never lights up the notification bar.No
ringing10Being offered to an agent's extension.No
jumped10The call moved off this extension to another. Shown as "Transferred".No
extension-cancel10This extension stopped ringing while the call continues elsewhere. Shown as "Extension cancellation".No
answered50An agent picked up. Shown as "In progress". Blocks every weight-10 status from here on.No
canceled100The caller hung up before anyone answered. Counts as a missed call.Against weight-10 and answered only
no-answer100Nobody picked up. Counts as a missed call.Against weight-10 and answered only
failed100The call could not be established. Counts as a missed call.Against weight-10 and answered only
completed150The call ran and hung up normally. Nothing of lower weight lands afterwards.Yes, against every lower weight — but another completed is equal weight and is still accepted, rewriting duration, end_time and caller_id. Send it twice and the second one wins.

Body

idstringRequired

Your identifier for the call, stable across every event of the same call. Letters, digits, spaces and the characters #, +, (, ), _, . and - only — anything else is rejected. It need only be unique within the integration sending it.

directionstringRequired

outbound is accepted as an alias for outbound-dial.

Allowed values:inboundoutbound-dialoutbound-auto
statusstringRequired

Where the call has got to. Matched case-insensitively, with underscores treated as hyphens — NO_ANSWER and no-answer are the same value. Each value carries a weight that decides whether it may overwrite the status already on the call; the table above the parameters lists what each one means, which replace which, and which one ends the call for good.

Allowed values:queuedringingansweredcompletedno-answercanceledfailedjumpedextension-cancel
fromstringOptional

The caller's number. Required when direction is inbound. Full international format matches customers most reliably.

tostringOptional

The number dialled. Required on outbound calls — that is, on anything whose direction is not inbound.

caller_idstringOptional

The extension or agent identifier handling the call. WiseParts resolves which user took the call from this value, using the extension-to-user mapping on the integration screen. An unmapped value still records the call; it just is not attributed to a user. Letters, digits, spaces and #, +, (, ), _ only — narrower than id, which also allows dots and dashes.

start_timestringOptional

When the call was answered, in RFC 3339. The first value a call receives is final. Leave it out on an answered event and WiseParts stamps the current time in the account's timezone, which is the usual way to send it. A value that cannot be parsed is a 422.

end_timestringOptional

When the call finished, in RFC 3339. Leave it out on a completed event and WiseParts stamps the current time in the account's timezone. A value that cannot be parsed is a 422.

durationintegerOptional

Call length in seconds. Decimals are accepted but truncated, so send whole seconds unless you have a reason not to.

forwarded_fromstringOptional

The number the call was diverted from.

parent_call_idstringOptional

The original call's id, when this call is a transfer. Same character restrictions as id.

cURL
curl --request POST \
  --url https://app.wiseparts.ai/api/v1/voice/call \
  --header 'Authorization: Bearer REPLACE_BEARER_TOKEN' \
  --header 'content-type: application/json' \
  --data '{"id":"CALL-2026-08-12-0042","direction":"inbound","status":"answered","from":"+351210000000","caller_id":"201","start_time":"2026-08-12T14:33:07+01:00"}'
JavaScript
const options = {
  method: 'POST',
  headers: {
    Authorization: 'Bearer REPLACE_BEARER_TOKEN',
    'content-type': 'application/json'
  },
  body: '{"id":"CALL-2026-08-12-0042","direction":"inbound","status":"answered","from":"+351210000000","caller_id":"201","start_time":"2026-08-12T14:33:07+01:00"}'
};

fetch('https://app.wiseparts.ai/api/v1/voice/call', options)
  .then(response => response.json())
  .then(response => console.log(response))
  .catch(err => console.error(err));
PHP
<?php

$curl = curl_init();

curl_setopt_array($curl, [
  CURLOPT_URL => "https://app.wiseparts.ai/api/v1/voice/call",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{\"id\":\"CALL-2026-08-12-0042\",\"direction\":\"inbound\",\"status\":\"answered\",\"from\":\"+351210000000\",\"caller_id\":\"201\",\"start_time\":\"2026-08-12T14:33:07+01:00\"}",
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer REPLACE_BEARER_TOKEN",
    "content-type: application/json"
  ],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
Python
import requests

url = "https://app.wiseparts.ai/api/v1/voice/call"

payload = {
    "id": "CALL-2026-08-12-0042",
    "direction": "inbound",
    "status": "answered",
    "from": "+351210000000",
    "caller_id": "201",
    "start_time": "2026-08-12T14:33:07+01:00"
}
headers = {
    "Authorization": "Bearer REPLACE_BEARER_TOKEN",
    "content-type": "application/json"
}

response = requests.request("POST", url, json=payload, headers=headers)

print(response.text)
Go
package main

import (
    "fmt"
    "strings"
    "net/http"
    "io/ioutil"
)

func main() {

    url := "https://app.wiseparts.ai/api/v1/voice/call"

    payload := strings.NewReader("{\"id\":\"CALL-2026-08-12-0042\",\"direction\":\"inbound\",\"status\":\"answered\",\"from\":\"+351210000000\",\"caller_id\":\"201\",\"start_time\":\"2026-08-12T14:33:07+01:00\"}")

    req, _ := http.NewRequest("POST", url, payload)

    req.Header.Add("Authorization", "Bearer REPLACE_BEARER_TOKEN")
    req.Header.Add("content-type", "application/json")

    res, _ := http.DefaultClient.Do(req)

    defer res.Body.Close()
    body, _ := ioutil.ReadAll(res.Body)

    fmt.Println(res)
    fmt.Println(string(body))

}
Ruby
require 'uri'
require 'net/http'
require 'openssl'

url = URI("https://app.wiseparts.ai/api/v1/voice/call")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer REPLACE_BEARER_TOKEN'
request["content-type"] = 'application/json'
request.body = "{\"id\":\"CALL-2026-08-12-0042\",\"direction\":\"inbound\",\"status\":\"answered\",\"from\":\"+351210000000\",\"caller_id\":\"201\",\"start_time\":\"2026-08-12T14:33:07+01:00\"}"

response = http.request(request)
puts response.read_body

Responses

200

The event was accepted. An event rejected by the forward-only rule, and one a handler chose to skip, answer exactly the same way — treat this as an acknowledgement of receipt rather than proof that a call was written.

401

The token is missing, wrong, or belongs to an integration that is not active. Requests that fail here leave no entry in the request history, because the account could not be identified. POST /voice/call and /voice/priority always answer with this JSON body. GET /voice only does so when the request asks for JSON — send Accept: application/json or you will get a 302 redirect to a browser page instead of a 401, which is a confusing thing to debug from a script.

422

One or more fields of the call event were missing or invalid. errors is keyed by field name with one or more messages each, and the keys are the field names in this reference rather than whatever your phone system called them — WiseParts normalises the payload before validating it.

500

Two causes, and only one of them worth retrying. Events for the same call id are serialised behind a five-second lock, and a burst that cannot take it in time fails here — that one is transient, so retry it. The other is a token that matches active Voice integrations on two different accounts: WiseParts refuses to guess which account the event belongs to and fails the request. That is stored configuration, not a moment of contention, so retrying never succeeds — the token has to be changed on one of the two accounts. Two integrations on the same account sharing a token never reach this. WiseParts picks one of them without comment, so the event lands on whichever it picked; the X-Centrix-Request-Type header on the 200 is how you find out which.

Response fields

resultstring

Forty-eight random alphanumeric characters, generated fresh for every request and stored nowhere. It identifies nothing and correlates with nothing, so do not keep it as a reference to the call — your own id is that reference.

200
{
  "result": "4HWYBVSKu4spptgZG9BKMszhlIWqlgp1bAJGtvUKpDHTSzjs"
}
401
{
  "message": "Unauthenticated."
}
422 missing-fields
{
  "message": "The id field is required. (and 3 more errors)",
  "errors": {
    "id": [
      "The id field is required."
    ],
    "to": [
      "The to field is required."
    ],
    "status": [
      "The status field is required."
    ],
    "direction": [
      "The direction field is required."
    ]
  }
}
422 unparseable-time
{
  "message": "The start_time does not match the format RFC 3339.",
  "errors": {
    "start_time": [
      "The start_time does not match the format RFC 3339."
    ]
  }
}
500 lock-timeout
{
  "message": "Server Error"
}
500 duplicate-token-across-accounts
{
  "message": "Server Error"
}