SIP call testing in your release pipeline
Run real SIP scenarios from GitHub Actions or Azure DevOps—no SIP stack in your build agent.
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.
- Create scenarios in Call Dashboard
- Group them into a test suite (e.g. slug
pre-release) - Create an API key in Account Settings
- Add
VTB_API_KEY,VTB_BASE_URL, andVTB_SUITEto 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
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
| Endpoint | Purpose |
|---|---|
| GET /scenarios/{id}/health | Single-scenario health gate |
| POST /scenarios/{id}/check | Trigger ad-hoc check |
| GET /scenarios/{id}/check-status | Poll check status |
| GET /suites/{slug}/health | Suite health gate |
| POST /suite-runs | Run 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.