Voisnap Docs
Guides

Telephony Debugging

Admin-only debug endpoints for triaging telephony issues — call inspector, credentials probe, and inbound webhook simulator.

Telephony Debugging

Voisnap.Admin.API exposes three operator-facing endpoints under /api/v1/telephony/debug for triaging telephony issues without bouncing through Cloud Logging. All require an admin bearer token (Authorize(Policy = "AdminOnly")).

The simulator additionally requires the Development environment — it returns 403 in staging/production to keep webhook simulation out of the production attack surface.

EndpointMethodPurpose
/api/v1/telephony/debug/calls/{callSid}GETCall inspector — joins Conversation + OutboundCall by provider id
/api/v1/telephony/debug/validate-connectionPOSTCredentials probe against a provider
/api/v1/telephony/debug/simulate-inboundPOSTDev-only TeXML/TwiML/NCCO preview
/api/v1/telephony/debug/repair-provider-sidsPOSTTelnyx: rewrite stored ProviderSid to the canonical phone_number.id

1. Call Inspector — GET calls/{callSid}

Use when: "The customer says they called but I can't find anything," or you need to see status/outcome/cost for a specific call without writing SQL.

Takes the provider-issued id (Twilio CallSid, Telnyx call_control_id, Vonage uuid) and returns whatever the database knows about it, joined from both Conversations.ExternalSessionId and OutboundCalls.CallSid.

Request

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  https://admin.api.voisnap.ai/api/v1/telephony/debug/calls/CA1234567890abcdef

Response (match)

{
  "callSid": "CA1234567890abcdef",
  "conversation": {
    "id": "…",
    "tenantId": "…",
    "agentId": "…",
    "channel": "Voice",
    "status": "Completed",
    "startedAt": "2026-05-28T12:01:00Z",
    "endedAt": "2026-05-28T12:03:42Z",
    "durationSeconds": 162,
    "callerIdentifier": "+14155551234",
    "outcome": "Resolved",
    "costTotal": 0.0834,
    "messageCount": 24
  },
  "outboundCall": null
}

For outbound calls, outboundCall is populated (with to, from, status, provider, retryCount, error, timestamps) and conversation may be null if the leg never connected.

Response (no match)

404 with a triage hint:

{
  "callSid": "CA1234567890abcdef",
  "message": "No Conversation or OutboundCall found for this id. The webhook may not have reached the API, the agent may not have been resolved, or the call may belong to a different region. Check Cloud Logging filtered by CallSid."
}

When this happens, in order:

  1. Confirm the region — the call may live in mewest.admin.api.voisnap.ai not the NY admin.
  2. In Cloud Logging, filter on the CallSid to see whether the webhook even arrived.
  3. Check agentId/tenantId resolution — calls that don't map to a known phone number get dropped before persistence.

2. Credentials Probe — POST validate-connection

Use when: A new provider's keys were just added, or you suspect a 401 storm. Makes a low-cost authenticated call (a pricing lookup) so credential failure surfaces immediately instead of when the next real call comes in.

Request

curl -X POST \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"provider": "Telnyx", "countryCode": "US"}' \
  https://admin.api.voisnap.ai/api/v1/telephony/debug/validate-connection

provider accepts Twilio, Vonage, Telnyx. countryCode defaults to US.

Response (success)

{
  "provider": "Telnyx",
  "success": true,
  "probeCountry": "US",
  "detail": "Authenticated probe returned pricing for US: localMonthlyCost=1.00"
}

Response (failure)

Returns 200 with success: false — credentials probing is itself a debug operation, not an error.

{
  "provider": "Vonage",
  "success": false,
  "detail": "Vonage:ApiKey is not configured"
}

Common failure causes by message:

DetailCause
<Provider>:ApiKey is not configuredCloud Run secret missing or not bound to the service
Unauthorized / 401Wrong key/secret pair, or revoked
Forbidden / 403Key exists but lacks the required scope (e.g. messaging-only)
HTTP timeoutNetwork egress blocked, or provider regional outage

3. Inbound Simulator — POST simulate-inbound (dev only)

Use when: Adding a new provider, changing TeXML/TwiML/NCCO generation, or debugging "the call connects but no media flows." Returns exactly the response body the platform would emit, with no webhook signature, no DB writes, no side effects.

Returns 403 Forbidden outside the Development environment.

Request

curl -X POST \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "Twilio",
    "agentId": "11111111-1111-1111-1111-111111111111",
    "tenantId": "22222222-2222-2222-2222-222222222222",
    "from": "+14155551234",
    "to": "+18005550000",
    "host": "localhost:5001"
  }' \
  http://localhost:5001/api/v1/telephony/debug/simulate-inbound

Optional: callSid (defaults to SIMULATED-<guid>), isSipTrunk (defaults to false).

Response

{
  "provider": "Twilio",
  "mediaStreamUrl": "wss://localhost:5001/api/v1/webhooks/twilio/media-stream?agentId=…&tenantId=…",
  "responseBody": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n  <Connect>\n    <Stream url=\"\">…</Stream>\n  </Connect>\n</Response>"
}

The responseBody is the exact TwiML / NCCO / TeXML the provider would receive on a real inbound webhook. Inspect it for:

  • Correct media-stream URL — wrong host or scheme means the carrier will never connect the WebSocket.
  • Custom parameters / headersagentId, tenantId, callerNumber, etc., must arrive in the WebSocket "start" event for the runtime to bind the call to an agent.
  • Connection type marker — for SIP trunks, the connectionType=sip parameter (Twilio) or equivalent must be present, otherwise the channel adapter falls back to PSTN audio codec settings.

4. Repair Telnyx ProviderSids — POST repair-provider-sids

Use when: assigning a Telnyx number to an agent logs Failed to update provider webhook with a Telnyx 404 Resource not found on PATCH /v2/phone_numbers/{id}. The stored ProviderSid is no longer (or never was) a real phone_number.id under the current API key — typically because the number was provisioned by an older code path that stored the number_order item id instead of the phone_number resource id.

For every active Telnyx number in the pool, this endpoint:

  1. Calls GET /v2/phone_numbers?filter[phone_number]=<E.164> to find Telnyx's canonical id.
  2. If it differs from the stored ProviderSid, rewrites the DB row.
  3. Reports per-number status (repaired, already-correct, missing-on-telnyx, error: …).

Request

# Dry run first — shows what *would* change without writing
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
  "https://admin.api.voisnap.ai/api/v1/telephony/debug/repair-provider-sids?dryRun=true"
 
# Live repair
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
  https://admin.api.voisnap.ai/api/v1/telephony/debug/repair-provider-sids

Response

{
  "dryRun": false,
  "scanned": 3,
  "repaired": 1,
  "alreadyCorrect": 2,
  "missingOnProvider": 0,
  "errors": 0,
  "items": [
    {
      "id": "deef0342-…",
      "phoneNumber": "+97283761547",
      "storedProviderSid": "de27c407-…",
      "canonicalProviderSid": "1234567890123456789",
      "action": "repaired"
    }
  ]
}

missing-on-telnyx means the number couldn't be found under the current API key — investigate whether the number was released, or whether the API key points at a different Telnyx account than the one that owns the number.

After running repair, re-assign the affected number to the agent: the webhook PATCH will now hit the correct phone_number.id and succeed.


Triage flow

A typical inbound-call outage walk-through:

  1. validate-connection — rule out credential drift first.
  2. calls/{callSid} — does the platform even know about the call? If 404, the webhook never landed; check provider dashboards + Cloud Logging.
  3. If the call landed but media never flowed: simulate-inbound in dev with the same agent/tenant and compare responseBody against what the provider received in their webhook logs.

For SMS or outbound issues, validate-connection covers credentials but the inspector is your fastest signal — outboundCall.error and retryCount typically tell the story.


These endpoints complement, not replace, the OpenTelemetry counters emitted from the providers themselves:

CounterWhen it increments
voisnap.telephony.api_errorsProvider REST call failed (tags: provider, method) — covers place-call, hangup, etc.
voisnap.telephony.media_stream_closedMedia-stream WebSocket closed (tag: reason)

When api_errors spikes for a single provider, validate-connection is the first thing to run.