API Reference
Complete REST API endpoint reference for CallMeter, including authentication, resource operations, request/response formats, pagination, and rate limits.
The CallMeter REST API provides programmatic access to projects, tests, test runs, probes, and registrars. Use the API to automate test execution, integrate with CI/CD pipelines, build custom dashboards, and export data.
API in Active Development
The API reference is being expanded. The endpoints documented below represent the current stable API surface. Contact support@callmeter.io for additional endpoint documentation or to request new API capabilities.
Base URL
All API requests use the following base URL:
https://callmeter.io/api/v1Authentication
Authenticate every request by including your API key in the Authorization header using the Bearer scheme:
curl -H "Authorization: Bearer cmk_your_api_key_here" \
https://callmeter.io/api/v1/projectsAPI keys use the cmk_ prefix followed by 32 hexadecimal characters. Generate keys from API Keys in the project sidebar. See API Authentication for detailed instructions on key creation, permissions, and security best practices.
Keep Your API Key Secret
API keys grant full access according to the creating user's permissions. Never commit API keys to version control, expose them in client-side code, or share them in unsecured channels. Use environment variables or a secrets manager.
Request Format
- Content-Type:
application/jsonfor request bodies - Accept:
application/jsonfor all responses - Method: Standard HTTP methods (GET, POST)
- URL parameters: Path parameters use
{param}notation (e.g.,/projects/{projectId}) - Query parameters: Used for filtering, pagination, and sorting
Response Format
All responses return JSON wrapped in a consistent envelope with data and meta fields.
Success response (single resource):
{
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"name": "Production Stress Test",
"status": "COMPLETED",
"createdAt": "2026-01-15T10:30:00Z"
},
"meta": {
"requestId": "uuid"
}
}Success response (list):
{
"data": [
{ "id": "abc123", "name": "Production Stress Test" },
{ "id": "def456", "name": "Staging Quality Check" }
],
"meta": {
"requestId": "uuid",
"pagination": {
"page": 1,
"perPage": 25,
"total": 42,
"totalPages": 2
}
}
}Error response:
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired API key"
},
"meta": {
"requestId": "uuid"
}
}HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created (resource created successfully) |
| 400 | Bad Request (invalid parameters) |
| 401 | Unauthorized (missing or invalid API key) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not Found (resource does not exist) |
| 422 | Unprocessable Entity (validation error) |
| 429 | Too Many Requests (rate limit exceeded) |
| 500 | Internal Server Error |
Pagination
List endpoints use offset-based pagination.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-indexed) |
per_page | integer | 25 | Number of items per page (max 100) |
Example:
# First page
curl -H "Authorization: Bearer cmk_..." \
"https://callmeter.io/api/v1/projects?page=1&per_page=10"
# Second page
curl -H "Authorization: Bearer cmk_..." \
"https://callmeter.io/api/v1/projects?page=2&per_page=10"When pagination.page equals pagination.totalPages, there are no more results.
Rate Limits
API requests are rate-limited per API key. When the rate limit is exceeded, the API returns a 429 Too Many Requests response with a Retry-After header indicating when to retry.
Rate limits are applied per API key. The limits that apply to your own organization are not published here -- ask us if you need them for capacity planning.
Projects
Projects are workspaces that group related tests, registrars, media files, and probes.
List Projects
List all projects accessible to the authenticated user.
GET /projectsQuery parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default 1) |
per_page | integer | No | Items per page (default 25, max 100) |
Response: 200 OK
{
"data": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"slug": "production",
"name": "Production",
"description": "Production SIP infrastructure testing",
"testCount": 12,
"probeCount": 3,
"registrarCount": 2
}
],
"meta": {
"requestId": "uuid",
"pagination": { "page": 1, "perPage": 25, "total": 1, "totalPages": 1 }
}
}Get Project
Retrieve a project by ID.
GET /projects/{projectId}Response: 200 OK with full project details including test count, probe count, and registrar count.
Tests
Tests represent reusable SIP testing scenario configurations. Each test can be run multiple times, producing independent test runs. All test endpoints are scoped under a project.
List Tests
List all tests in a project.
GET /projects/{projectId}/testsQuery parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default 1) |
per_page | integer | No | Items per page (default 25, max 100) |
Response: 200 OK
{
"data": [
{
"id": "abc123",
"name": "Production Stress Test",
"endpoints": 500,
"durationSeconds": 120,
"buildup": 30,
"lastRunStatus": "COMPLETED",
"createdAt": "2026-01-15T10:30:00Z",
"updatedAt": "2026-01-15T10:30:00Z"
}
],
"meta": {
"requestId": "uuid",
"pagination": { "page": 1, "perPage": 25, "total": 1, "totalPages": 1 }
}
}Get Test
Retrieve a single test configuration by ID.
GET /projects/{projectId}/tests/{testId}Response: 200 OK with full test configuration including groups, codecs, and media settings.
Run Test
Start a new execution of a test. Creates a test run and begins the allocation and execution process.
POST /projects/{projectId}/tests/{testId}/runResponse: 201 Created
{
"data": {
"id": "xyz789",
"testId": "abc123",
"status": "PENDING",
"createdAt": "2026-01-15T11:00:00Z"
},
"meta": {
"requestId": "uuid"
}
}The run transitions through PENDING, QUEUED, RUNNING, and then to one of the terminal states COMPLETED, FAILED, CANCELLED or CANNOT_RUN_FOR_NOW.
Test Runs
Test runs represent individual executions of a test. Each run has its own status, endpoints, and metrics. All test run endpoints are scoped under a project.
List Test Runs
List test runs in a project.
GET /projects/{projectId}/test-runsQuery parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default 1) |
per_page | integer | No | Items per page (default 25, max 100) |
Response: 200 OK
{
"data": [
{
"id": "xyz789",
"status": "COMPLETED",
"error": null,
"priority": 0,
"testId": "abc123",
"testName": "Nightly trunk check",
"endpoints": 500,
"submittedAt": "2026-01-15T11:00:00Z",
"startedAt": "2026-01-15T11:00:01Z",
"endedAt": "2026-01-15T11:02:35Z"
}
],
"meta": {
"requestId": "uuid",
"pagination": { "page": 1, "perPage": 25, "total": 1, "totalPages": 1 }
}
}Get Test Run
Retrieve a test run by ID, including status and summary statistics.
GET /projects/{projectId}/test-runs/{testRunId}Response: 200 OK
{
"data": {
"id": "xyz789",
"status": "COMPLETED",
"error": null,
"priority": 0,
"isProbeRun": false,
"testId": "abc123",
"testName": "Nightly trunk check",
"projectName": "Wholesale routes",
"organizationName": "Acme Telecom",
"endpoints": 500,
"durationSeconds": 120,
"submittedAt": "2026-01-15T11:00:00Z",
"startedAt": "2026-01-15T11:00:01Z",
"endedAt": "2026-01-15T11:02:35Z",
"summary": {
"avgMos": 4.21,
"minMos": 3.84,
"avgJitter": 8.3,
"maxJitter": 24.1,
"avgPacketLossFraction": 0.0012,
"totalPacketsLost": 431,
"avgRtt": 45.2,
"maxRtt": 98.7,
"avgRFactor": 89.4,
"minRFactor": 71.2,
"totalNackCount": 12,
"totalPliCount": 3,
"totalFirCount": 0,
"totalPlcEvents": 27,
"totalStreams": 996,
"streamsWithFeedback": 994
},
"callTiming": {
"totalCalls": 500,
"successCount": 486,
"timeoutCount": 4,
"rejectedCount": 8,
"errorCount": 2,
"inProgressCount": 0,
"asr": 97.2,
"ner": 98.8,
"pdd": { "p50": 812, "p95": 1640, "p99": 2210, "avg": 903.4, "min": 410, "max": 2380 },
"timeToTrying": { "p50": 42, "p95": 88, "p99": 140, "avg": 51.2, "min": 18, "max": 210 },
"timeToRinging": { "p50": 640, "p95": 1180, "p99": 1520, "avg": 702.5, "min": 310, "max": 1740 },
"timeToFirstMedia": { "p50": 180, "p95": 340, "p99": 480, "avg": 201.7, "min": 95, "max": 610 },
"callSetup": { "p50": 890, "p95": 1720, "p99": 2290, "avg": 981.3, "min": 470, "max": 2460 },
"callDuration": { "p50": 60000, "p95": 60200, "p99": 60400, "avg": 59980, "min": 12000, "max": 60500 },
"registerRtt": { "p50": 88, "p95": 165, "p99": 240, "avg": 96.4, "min": 41, "max": 310 }
},
"sipResponses": [
{ "statusCode": 200, "reasonPhrase": "OK", "count": 486, "percentage": 97.99 },
{ "statusCode": 503, "reasonPhrase": "Service Unavailable", "count": 6, "percentage": 1.21 },
{ "statusCode": 486, "reasonPhrase": "Busy Here", "count": 4, "percentage": 0.81 }
],
"subPopulationInstances": [
{
"id": "spi-001",
"endpoints": 500,
"status": "ALLOCATED",
"subPopulationId": "sp-001",
"worker": { "id": "w-001", "name": "eu-west-1", "ownership": "USER_OWNED" }
}
],
"counts": {
"events": 1042,
"sipMessages": 5980
}
},
"meta": {
"requestId": "uuid"
}
}List Test Run Endpoints
List endpoints in a test run with their final status and per-endpoint call timing. Use this to find which route degraded when a run-level gate fails.
GET /projects/{projectId}/test-runs/{testRunId}/endpointsQuery parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default 1) |
per_page | integer | No | Items per page (default 25, max 100) |
Response: 200 OK
{
"data": [
{
"id": "ep_001",
"endpointType": "CALLER",
"phase": "CLOSED",
"outcome": "SUCCESS",
"calleeUri": "sip:user002@sip.example.com",
"worker": { "id": "w-001", "name": "eu-west-1", "ownership": "USER_OWNED" },
"registrar": { "id": "r-001", "name": "Primary SBC", "uri": "sip:sbc.example.com" },
"callTiming": {
"timeToTryingMs": 42,
"timeToRingingMs": 640,
"pddMs": 812,
"callSetupMs": 890,
"timeToFirstMediaMs": 180,
"callDurationMs": 60000,
"registerRttMs": 88,
"finalStatusCode": 200,
"finalReasonPhrase": "OK",
"callResult": "SUCCESS"
}
}
],
"meta": {
"requestId": "uuid",
"pagination": { "page": 1, "perPage": 25, "total": 500, "totalPages": 20 }
}
}callTiming is null for an endpoint that never reached the timing collector — a call that failed
at registration, for instance. That is not the same as timings of zero, so it is not zero-filled.
Probes
Probes are scheduled, continuous monitoring tests. They execute at regular intervals and evaluate quality thresholds.
List Probes
List all probes in a project.
GET /projects/{projectId}/probesQuery parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default 1) |
per_page | integer | No | Items per page (default 25, max 100) |
Response: 200 OK
{
"data": [
{
"id": "probe_abc123",
"name": "Production Health Check",
"description": "Hourly check on the primary interconnect",
"mode": "CALL",
"status": "HEALTHY",
"enabled": true,
"scheduleType": "FIXED_INTERVAL",
"fixedIntervalSec": 900,
"cronExpression": null,
"timeOfDayStartUtc": null,
"timeOfDayEndUtc": null,
"timezone": "UTC",
"durationSeconds": 30,
"alertStrikeRuns": 2,
"parentProbeId": null,
"lastInstanceStatus": "COMPLETED",
"lastInstanceAt": "2026-01-15T11:00:00Z",
"escalateAfterMinutes": 60,
"autoPauseAfterHours": null,
"autoPausedAt": null,
"autoPauseReason": null,
"lastSkipReason": null,
"lastSkipAt": null,
"thresholds": [
{ "id": "th-001", "metricKey": "mos", "greenValue": 4.0, "orangeValue": 3.6, "enabled": true }
],
"createdAt": "2026-01-15T09:00:00Z",
"updatedAt": "2026-01-15T11:00:00Z"
}
],
"meta": {
"requestId": "uuid",
"pagination": { "page": 1, "perPage": 25, "total": 1, "totalPages": 1 }
}
}Get Probe
Retrieve a probe by ID, including current health status, configuration, and threshold settings.
GET /projects/{projectId}/probes/{probeId}Response: 200 OK with full probe configuration, current health status, last execution timestamp, and threshold definitions.
Reading a probe's operational state
status and enabled describe how a probe's last run went and whether it is switched on.
Neither tells you whether it is still executing, and a probe can be enabled: true,
status: "HEALTHY", and not have run for days. Two field pairs answer that:
| Field | Meaning when non-null |
|---|---|
lastSkipReason / lastSkipAt | The most recent scheduled run was refused before dispatch, typically by an organization usage or plan limit. Cleared automatically on the next successful run. |
autoPausedAt / autoPauseReason | An incident stayed open past autoPauseAfterHours, so the probe was disabled automatically. enabled is false in this state. |
If you are building a monitoring integration, check lastSkipReason before trusting
status. A non-null value means the probe stopped running while its last recorded status
stayed exactly as it was.
List Probe Incidents
Incident history for a probe, and for its multi-region sibling probes. An incident opens
once a probe has failed its configured number of consecutive runs (alertStrikeRuns) and
closes when the probe recovers.
GET /projects/{projectId}/probes/{probeId}/incidentsQuery parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default 1) |
per_page | integer | No | Items per page (default 25, max 100) |
severity | string | No | DEGRADED or DOWN |
state | string | No | open for incidents still in progress, resolved for ended ones. Omit for both. |
Results are ordered with open incidents first, then most recently started first.
Response: 200 OK
{
"data": [
{
"id": "3f8a1c22-9c1e-4f7a-8b2e-11a2b3c4d5e6",
"probeId": "9b2c4d61-77aa-4c3d-9f10-0a1b2c3d4e5f",
"region": "eu-west",
"severity": "DOWN",
"startedAt": "2026-01-15T02:14:00Z",
"endedAt": "2026-01-15T02:51:00Z",
"durationMs": 2220000,
"message": null
}
],
"meta": {
"requestId": "uuid",
"pagination": { "page": 1, "perPage": 25, "total": 1, "totalPages": 1 }
}
}`message` is always null today
The field is published because it exists on the incident record, but nothing in the platform writes it yet — incidents are opened with a severity and a start time and no text. Treat it as reserved, and do not build alerting logic that depends on it being non-null.
endedAt and durationMs are null while an incident is still open — use
?state=open to list only those. region is derived from a trailing (region) suffix on
the probe's name and is null for a probe that carries none.
List Probe Runs
Execution history for a probe, most recently scheduled first, with the per-metric threshold verdicts that produced each run's status.
GET /projects/{projectId}/probes/{probeId}/instancesQuery parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default 1) |
per_page | integer | No | Items per page (default 25, max 100) |
status | string | No | HEALTHY, DEGRADED, UNHEALTHY or UNKNOWN |
Response: 200 OK
{
"data": [
{
"id": "7c1d9e33-4b2a-4e88-9d31-2f4c6a8b0d12",
"status": "DEGRADED",
"testRunId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"scheduledAt": "2026-01-15T11:00:00Z",
"startedAt": "2026-01-15T11:00:02Z",
"completedAt": "2026-01-15T11:00:32Z",
"thresholdResults": [
{
"metricKey": "mos",
"displayName": "MOS",
"unit": "",
"value": 3.42,
"status": "DEGRADED",
"higherIsBetter": true
}
]
}
],
"meta": {
"requestId": "uuid",
"pagination": { "page": 1, "perPage": 25, "total": 1, "totalPages": 1 }
}
}thresholdResults is an empty array for a run that never completed and was therefore never
graded. testRunId links to the test run this probe execution produced, so
GET /projects/{projectId}/test-runs/{testRunId}/metrics expands any run into its full
quality detail.
Run Probe
Trigger an immediate execution of a probe, outside its regular schedule.
POST /projects/{projectId}/probes/{probeId}/runResponse: 201 Created with the queued probe execution details.
Registrars
Registrars represent SIP server configurations used by tests and probes.
List Registrars
List all registrars configured for a project.
GET /projects/{projectId}/registrarsQuery parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default 1) |
per_page | integer | No | Items per page (default 25, max 100) |
Response: 200 OK
{
"data": [
{
"id": "reg_abc123",
"name": "Production SBC",
"uri": "sip:pbx.example.com",
"protocol": "UDP",
"dns": false,
"outboundProxyUri": null,
"isCloud": false,
"sipAccountCount": 500,
"createdAt": "2026-01-15T09:00:00Z",
"updatedAt": "2026-01-15T09:00:00Z"
}
],
"meta": {
"requestId": "uuid",
"pagination": { "page": 1, "perPage": 25, "total": 1, "totalPages": 1 }
}
}Error Handling
All error responses follow a consistent format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid page parameter"
},
"meta": {
"requestId": "uuid"
}
}Common Error Codes
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid API key |
FORBIDDEN | 403 | Insufficient permissions for this operation |
NOT_FOUND | 404 | Resource does not exist or is not accessible |
VALIDATION_ERROR | 422 | Request body failed validation |
RATE_LIMITED | 429 | Rate limit exceeded. Check Retry-After header. |
INTERNAL_ERROR | 500 | Unexpected server error. Contact support if persistent. |
CI/CD Integration Pattern
A common pattern for integrating CallMeter into deployment pipelines:
# 1. Trigger a test run
RUN_ID=$(curl -s -X POST \
-H "Authorization: Bearer $CALLMETER_API_KEY" \
"https://callmeter.io/api/v1/projects/$PROJECT_ID/tests/$TEST_ID/run" \
| jq -r '.data.testRunId')
# 2. Poll until the run reaches a terminal state.
# COMPLETED is the only one worth evaluating. FAILED, CANCELLED and CANNOT_RUN_FOR_NOW are
# terminal too -- a loop that waits for them to become COMPLETED polls until its timeout and
# then reports a stall rather than what actually happened.
while true; do
STATUS=$(curl -s \
-H "Authorization: Bearer $CALLMETER_API_KEY" \
"https://callmeter.io/api/v1/projects/$PROJECT_ID/test-runs/$RUN_ID" \
| jq -r '.data.status')
case "$STATUS" in
COMPLETED) break ;;
FAILED|CANCELLED|CANNOT_RUN_FOR_NOW)
echo "Test ended with status: $STATUS"
exit 1
;;
*) sleep 10 ;;
esac
done
# 3. Check quality gate
RUN=$(curl -s \
-H "Authorization: Bearer $CALLMETER_API_KEY" \
"https://callmeter.io/api/v1/projects/$PROJECT_ID/test-runs/$RUN_ID")
# A run that placed no calls has nothing to judge, and avgMos would read 0 -- which fails for
# the wrong reason and sends you to debug audio quality on a trunk that was never dialled.
TOTAL_CALLS=$(echo "$RUN" | jq -r '.data.callTiming.totalCalls')
if [ "$TOTAL_CALLS" -eq 0 ]; then
echo "Quality gate FAILED: the run placed no calls - check registration"
exit 1
fi
MOS=$(echo "$RUN" | jq -r '.data.summary.avgMos')
# awk, not bc. bc is absent from alpine, debian-slim and the official node images, and
# `(( $(... | bc -l) ))` with bc missing evaluates an EMPTY expression, which is false -- so the
# gate reports PASS on every run, including the bad ones.
if awk -v a="$MOS" -v b=3.5 'BEGIN { exit !(a < b) }'; then
echo "Quality gate FAILED: MOS $MOS is below 3.5"
exit 1
fi
echo "Quality gate PASSED: MOS $MOS"See CI/CD Integration for complete pipeline examples.
Related Pages
- API Authentication -- API key management
- CI/CD Integration -- Pipeline integration examples
- Probe Health States -- Probe health evaluation
API Authentication
Authenticate with the CallMeter API using API keys, manage key lifecycle, and follow security best practices.
MCP Server
Connect your own AI assistant or agent to your CallMeter account data using the Model Context Protocol, authenticated with your existing API key and limited to what that key can already read.