SIP call testing in your release pipeline

Run real SIP scenarios from GitHub Actions or Azure DevOps—no SIP stack in your build agent.

GitHub Actions Azure DevOps API key auth Pass / fail gates

Health gate

Ask: did scheduled monitoring pass in the last X minutes? Use before deploy when scenarios already run on an interval.

vtb-sip-check.sh gate \
  --suite pre-release \
  --max-age-seconds 600

Ad-hoc run

Trigger one scenario and wait for the result. Useful right after a deployment.

vtb-sip-check.sh run \
  --scenario-id 42 \
  --wait --timeout 300

Suite batch

Run a named suite of scenarios and get one aggregate pass/fail for release sign-off.

vtb-sip-check.sh suite \
  --name pre-release \
  --wait --timeout 300

How it works

Your CI agent calls the VoIP Toolbox Integration API over HTTPS. We queue real SIP checks on our probe infrastructure (SIPp, Kamailio, rtpengine). Results are stored and returned to your pipeline.

  1. Create scenarios in Call Dashboard
  2. Group them into a test suite (e.g. slug pre-release)
  3. Create an API key in Account Settings
  4. Add VTB_API_KEY, VTB_BASE_URL, and VTB_SUITE to pipeline secrets

vtb-sip-check.sh

A small bash helper for GitHub Actions and Azure DevOps. It wraps the Integration API with gate, run, and suite commands. Requires curl and jq on the build agent.

Set VTB_API_KEY and VTB_BASE_URL, then vendor the script into your repo or pipe it from this server:

curl -fsSL "https://voiptoolbox.net/scripts/ci/vtb-sip-check.sh" -o vtb-sip-check.sh
chmod +x vtb-sip-check.sh
Download
View full source code
#!/usr/bin/env bash
# VoIP Toolbox SIP CI helper — requires curl and jq.
# Env: VTB_API_KEY (required), VTB_BASE_URL (required, no trailing slash)
# Exit: 0 pass, 1 fail, 2 usage, 3 timeout, 4 API error

set -euo pipefail

VTB_API_KEY="${VTB_API_KEY:-}"
VTB_BASE_URL="${VTB_BASE_URL:-}"
JSON_OUTPUT=0
MODE=""
SUITE=""
SCENARIO_ID=""
MAX_AGE=""
WAIT=0
TIMEOUT=300
POLL_INTERVAL=10

usage() {
  cat <<'EOF'
Usage:
  vtb-sip-check.sh gate --suite SLUG --max-age-seconds SECONDS
  vtb-sip-check.sh gate --scenario-id ID --max-age-seconds SECONDS
  vtb-sip-check.sh run --scenario-id ID [--wait] [--timeout SECONDS]
  vtb-sip-check.sh suite --name SLUG [--wait] [--timeout SECONDS]

Environment:
  VTB_API_KEY     Account API key (vtb_…)
  VTB_BASE_URL    App base URL (e.g. https://app.voiptoolbox.net)

Options:
  --json          Print JSON summary on stdout
EOF
  exit 2
}

die_api() {
  echo "API error: $1" >&2
  exit 4
}

api_get() {
  local path="$1"
  local resp
  resp=$(curl -sS -w "\n%{http_code}" -H "Authorization: Bearer ${VTB_API_KEY}" -H "Accept: application/json" "${VTB_BASE_URL}${path}")
  HTTP_BODY=$(echo "$resp" | sed '$d')
  HTTP_CODE=$(echo "$resp" | tail -n1)
  if [[ "$HTTP_CODE" -ge 400 ]]; then
    die_api "GET ${path} returned ${HTTP_CODE}: ${HTTP_BODY}"
  fi
  echo "$HTTP_BODY"
}

api_post() {
  local path="$1"
  local data="$2"
  local resp
  resp=$(curl -sS -w "\n%{http_code}" -X POST -H "Authorization: Bearer ${VTB_API_KEY}" -H "Content-Type: application/json" -H "Accept: application/json" -d "$data" "${VTB_BASE_URL}${path}")
  HTTP_BODY=$(echo "$resp" | sed '$d')
  HTTP_CODE=$(echo "$resp" | tail -n1)
  if [[ "$HTTP_CODE" -ge 400 && "$HTTP_CODE" != "504" ]]; then
    die_api "POST ${path} returned ${HTTP_CODE}: ${HTTP_BODY}"
  fi
  echo "$HTTP_BODY"
}

[[ $# -ge 1 ]] || usage
MODE="$1"
shift

while [[ $# -gt 0 ]]; do
  case "$1" in
    --suite) SUITE="$2"; shift 2 ;;
    --name) SUITE="$2"; shift 2 ;;
    --scenario-id) SCENARIO_ID="$2"; shift 2 ;;
    --max-age-seconds) MAX_AGE="$2"; shift 2 ;;
    --wait) WAIT=1; shift ;;
    --timeout) TIMEOUT="$2"; shift 2 ;;
    --poll-interval) POLL_INTERVAL="$2"; shift 2 ;;
    --json) JSON_OUTPUT=1; shift ;;
    *) usage ;;
  esac
done

[[ -n "$VTB_API_KEY" && -n "$VTB_BASE_URL" ]] || usage

case "$MODE" in
  gate)
    [[ -n "$MAX_AGE" ]] || usage
    if [[ -n "$SUITE" ]]; then
      BODY=$(api_get "/api/integration/v1/suites/${SUITE}/health?max_age_seconds=${MAX_AGE}")
      HEALTHY=$(echo "$BODY" | jq -r '.healthy')
      if [[ "$JSON_OUTPUT" -eq 1 ]]; then echo "$BODY"; fi
      if [[ "$HEALTHY" == "true" ]]; then
        echo "Suite ${SUITE} is healthy (max_age=${MAX_AGE}s)"
        exit 0
      fi
      echo "Suite ${SUITE} is NOT healthy" >&2
      echo "$BODY" | jq -r '.members[]? | select(.healthy==false) | "- \(.name // .endpoint_id): \(.reasons | join(", "))"' >&2 || true
      exit 1
    elif [[ -n "$SCENARIO_ID" ]]; then
      BODY=$(api_get "/api/integration/v1/scenarios/${SCENARIO_ID}/health?max_age_seconds=${MAX_AGE}")
      HEALTHY=$(echo "$BODY" | jq -r '.healthy')
      if [[ "$JSON_OUTPUT" -eq 1 ]]; then echo "$BODY"; fi
      if [[ "$HEALTHY" == "true" ]]; then
        echo "Scenario ${SCENARIO_ID} is healthy (max_age=${MAX_AGE}s)"
        exit 0
      fi
      echo "Scenario ${SCENARIO_ID} is NOT healthy: $(echo "$BODY" | jq -r '.reasons | join(", ")')" >&2
      exit 1
    else
      usage
    fi
    ;;
  run)
    [[ -n "$SCENARIO_ID" ]] || usage
    TRIGGER=$(api_post "/api/integration/v1/scenarios/${SCENARIO_ID}/check" '{}')
    TRIGGERED_AT=$(echo "$TRIGGER" | jq -r '.triggered_at // empty')
    if [[ "$WAIT" -eq 0 ]]; then
      [[ "$JSON_OUTPUT" -eq 1 ]] && echo "$TRIGGER"
      echo "Check queued for scenario ${SCENARIO_ID}"
      exit 0
    fi
    DEADLINE=$(( $(date +%s) + TIMEOUT ))
    while [[ $(date +%s) -lt $DEADLINE ]]; do
      STATUS=$(api_get "/api/integration/v1/scenarios/${SCENARIO_ID}/check-status")
      PENDING=$(echo "$STATUS" | jq -r '.pending_check_status // empty')
      CHECKED=$(echo "$STATUS" | jq -r '.latest_result.checked_at // empty')
      OK=$(echo "$STATUS" | jq -r '.latest_result.is_successful // false')
      if [[ -z "$PENDING" && -n "$CHECKED" && "$CHECKED" > "$TRIGGERED_AT" || "$CHECKED" == "$TRIGGERED_AT" ]]; then
        if [[ "$OK" == "true" ]]; then
          [[ "$JSON_OUTPUT" -eq 1 ]] && echo "$STATUS"
          echo "Scenario ${SCENARIO_ID} passed"
          exit 0
        fi
        [[ "$JSON_OUTPUT" -eq 1 ]] && echo "$STATUS"
        echo "Scenario ${SCENARIO_ID} failed" >&2
        exit 1
      fi
      sleep "$POLL_INTERVAL"
    done
    echo "Timed out waiting for scenario ${SCENARIO_ID}" >&2
    exit 3
    ;;
  suite)
    [[ -n "$SUITE" ]] || usage
    PAYLOAD=$(jq -n --arg suite "$SUITE" --argjson wait "$WAIT" --argjson timeout "$TIMEOUT" \
      '{suite: $suite, wait: ($wait == 1), timeout_seconds: $timeout}')
    BODY=$(api_post "/api/integration/v1/suite-runs" "$PAYLOAD")
    STATUS=$(echo "$BODY" | jq -r '.status // empty')
    if [[ "$WAIT" -eq 1 ]]; then
      [[ "$JSON_OUTPUT" -eq 1 ]] && echo "$BODY"
      case "$STATUS" in
        passed)
          echo "Suite ${SUITE} passed ($(echo "$BODY" | jq -r '.passed')/$(echo "$BODY" | jq -r '.passed + .failed + .timed_out'))"
          exit 0
          ;;
        timed_out)
          echo "Suite ${SUITE} timed out" >&2
          exit 3
          ;;
        *)
          echo "Suite ${SUITE} failed ($(echo "$BODY" | jq -r '.failed') failed)" >&2
          exit 1
          ;;
      esac
    else
      [[ "$JSON_OUTPUT" -eq 1 ]] && echo "$BODY"
      echo "Suite run queued: $(echo "$BODY" | jq -r '.suite_run_id')"
      exit 0
    fi
    ;;
  *)
    usage
    ;;
esac

Integration recipes

curl — suite health gate

curl -s -H "Authorization: Bearer vtb_YOUR_KEY" \
  "https://voiptoolbox.net/api/integration/v1/suites/pre-release/health?max_age_seconds=600"

curl — trigger suite run (wait for results)

curl -s -X POST -H "Authorization: Bearer vtb_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"suite":"pre-release","wait":true,"timeout_seconds":300}' \
  "https://voiptoolbox.net/api/integration/v1/suite-runs"

GitHub Actions

- name: SIP suite before deploy
  env:
    VTB_API_KEY: {{ secrets.VTB_API_KEY }}
    VTB_BASE_URL: https://voiptoolbox.net
  run: |
    curl -fsSL "$VTB_BASE_URL/scripts/ci/vtb-sip-check.sh" | bash -s -- \
      suite --name "{{ vars.VTB_SUITE }}" --wait --timeout 300

vtb-sip-check.sh — examples

# Health gate (scheduled monitoring must be green)
vtb-sip-check.sh gate --suite pre-release --max-age-seconds 600

# Ad-hoc suite run
vtb-sip-check.sh suite --name pre-release --wait --timeout 300

# Single scenario
vtb-sip-check.sh run --scenario-id 42 --wait --timeout 300

View full source code with copy and download.

Azure DevOps

- bash: ./vtb-sip-check.sh gate --suite $(VTB_SUITE) --max-age-seconds 600
  env:
    VTB_API_KEY: $(VTB_API_KEY)
    VTB_BASE_URL: https://voiptoolbox.net

Advanced: pass scenario_ids instead of a suite slug on POST /suite-runs.

API reference

Full OpenAPI spec: Integration API

EndpointPurpose
GET /scenarios/{id}/healthSingle-scenario health gate
POST /scenarios/{id}/checkTrigger ad-hoc check
GET /scenarios/{id}/check-statusPoll check status
GET /suites/{slug}/healthSuite health gate
POST /suite-runsRun suite or scenario list
GET /suite-runs/{id}Poll async suite run

FAQ

Rate limits
60 requests per minute per account on the Integration API.
Timeouts
Test Call scenarios with RTP may take several minutes; default wait timeout is 300 seconds.
Scheduled vs CI-triggered
Keep endpoints on a check interval for continuous monitoring; use health gates to trust recent greens, or suite runs to force fresh checks before release.
Build correlation
Pass client_reference (e.g. build ID) on check and suite-run requests to attach evidence to a pipeline run.