> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cekura.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing Outbound Calls

> This guide walks you through the process of testing outbound calls using evaluators. You'll learn how to retrieve evaluators with phone numbers and execute outbound call tests.

export const CopyPageButton = () => {
  if (typeof window !== 'undefined') {
    setTimeout(function () {
      if (document.getElementById('ck-tools')) return;
      var anchor = document.getElementById('content-area') || document.querySelector('.mdx-content');
      if (!anchor) return;
      if (!document.getElementById('ck-style')) {
        var s = document.createElement('style');
        s.id = 'ck-style';
        s.textContent = '#ck-tools{position:absolute;top:6px;right:0;z-index:100;font-family:inherit;}' + '.ck-row{display:inline-flex;align-items:stretch;border:1px solid rgba(0,0,0,0.15);border-radius:8px;overflow:hidden;background:#fff;}' + ':root.dark .ck-row{background:rgba(255,255,255,0.06);border-color:rgba(255,255,255,0.12);}' + '.ck-btn{padding:5px 12px;border:none;background:none;cursor:pointer;font-size:13px;font-weight:500;font-family:inherit;color:#374151;}' + ':root.dark .ck-btn{color:#d1d5db;}' + '.ck-btn:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-btn:hover{background:rgba(255,255,255,0.06);}' + '.ck-chevron{padding:5px 8px;border:none;background:none;cursor:pointer;font-size:14px;font-family:inherit;color:#374151;}' + ':root.dark .ck-chevron{color:#d1d5db;}' + '.ck-chevron:hover{background:rgba(0,0,0,0.04);}' + ':root.dark .ck-chevron:hover{background:rgba(255,255,255,0.06);}' + '.ck-divider{width:1px;background:rgba(0,0,0,0.12);flex-shrink:0;}' + ':root.dark .ck-divider{background:rgba(255,255,255,0.12);}' + '.ck-dd{position:absolute;top:calc(100% + 4px);right:0;min-width:180px;background:#fff;border:1px solid rgba(0,0,0,0.12);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);padding:4px;display:none;z-index:200;}' + ':root.dark .ck-dd{background:#1f2937;border-color:rgba(255,255,255,0.1);box-shadow:0 4px 16px rgba(0,0,0,0.35);}' + '.ck-item{display:block;width:100%;padding:7px 12px;border:none;background:none;border-radius:6px;cursor:pointer;font-size:13px;font-family:inherit;text-align:left;color:#374151;}' + ':root.dark .ck-item{color:#d1d5db;}' + '.ck-item:hover{background:rgba(0,0,0,0.05);}' + ':root.dark .ck-item:hover{background:rgba(255,255,255,0.07);}';
        document.head.appendChild(s);
      }
      var wrap = document.createElement('div');
      wrap.id = 'ck-tools';
      var row = document.createElement('div');
      row.className = 'ck-row';
      var mainBtn = document.createElement('button');
      mainBtn.className = 'ck-btn';
      mainBtn.textContent = 'Copy page';
      var divider = document.createElement('span');
      divider.className = 'ck-divider';
      var chevron = document.createElement('button');
      chevron.className = 'ck-chevron';
      chevron.textContent = '▾';
      var dd = document.createElement('div');
      dd.className = 'ck-dd';
      function closeDD() {
        dd.style.display = 'none';
      }
      function openDD() {
        dd.style.display = 'block';
      }
      chevron.onclick = function (e) {
        e.stopPropagation();
        if (dd.style.display === 'block') {
          closeDD();
        } else {
          openDD();
        }
      };
      document.addEventListener('click', function (e) {
        if (!e.target.closest('#ck-tools')) {
          closeDD();
        }
      });
      document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape') {
          closeDD();
        }
      });
      function makeItem(label, fn) {
        var b = document.createElement('button');
        b.className = 'ck-item';
        b.textContent = label;
        b.onclick = function () {
          fn();
          closeDD();
        };
        return b;
      }
      function getMarkdown() {
        var walk = function (node) {
          if (!node) return '';
          if (node.nodeType === 3) return node.textContent || '';
          if (node.nodeType !== 1) return '';
          var tag = node.tagName.toLowerCase();
          var skip = ['script', 'style', 'svg', 'noscript', 'button', 'iframe'];
          if (skip.indexOf(tag) !== -1) return '';
          if (node.id === 'ck-tools') return '';
          var ch = Array.from(node.childNodes).map(walk).join('');
          if (tag === 'h1') return '\n# ' + ch.trim() + '\n\n';
          if (tag === 'h2') return '\n## ' + ch.trim() + '\n\n';
          if (tag === 'h3') return '\n### ' + ch.trim() + '\n\n';
          if (tag === 'p') return '\n' + ch.trim() + '\n\n';
          if (tag === 'pre') return '\n```\n' + node.textContent.trim() + '\n```\n\n';
          if (tag === 'li') return '- ' + ch.trim() + '\n';
          if (tag === 'code') return '`' + ch.trim() + '`';
          return ch;
        };
        var content = document.querySelector('.mdx-content') || document.getElementById('content-area') || document.body;
        return walk(content).replace(/\n\n\n+/g, '\n\n').trim();
      }
      function copyMd() {
        var md = getMarkdown();
        navigator.clipboard.writeText(md).then(function () {
          mainBtn.textContent = 'Copied!';
          setTimeout(function () {
            mainBtn.textContent = 'Copy page';
          }, 2000);
        });
      }
      function viewMd() {
        var md = getMarkdown();
        var safe = md.split('&').join('&amp;').split('<').join('&lt;').split('>').join('&gt;');
        var html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{font-family:monospace;max-width:860px;margin:40px auto;padding:0 24px;line-height:1.7;white-space:pre-wrap;word-wrap:break-word}</style></head><body>' + safe + '</body></html>';
        window.open(URL.createObjectURL(new Blob([html], {
          type: 'text/html'
        })), '_blank');
      }
      function openClaude() {
        var prompt = 'Can you read this Cekura docs page ' + window.location.href + ' so I can ask you questions?';
        window.open('https://claude.ai/new?q=' + encodeURIComponent(prompt), '_blank');
      }
      mainBtn.onclick = copyMd;
      dd.appendChild(makeItem('Copy page', copyMd));
      dd.appendChild(makeItem('View as Markdown', viewMd));
      dd.appendChild(makeItem('Open in Claude', openClaude));
      row.appendChild(mainBtn);
      row.appendChild(divider);
      row.appendChild(chevron);
      wrap.appendChild(row);
      wrap.appendChild(dd);
      anchor.style.position = 'relative';
      anchor.insertBefore(wrap, anchor.firstChild);
    }, 50);
  }
  return null;
};

<CopyPageButton />

<iframe width="100%" style={{ aspectRatio: '16 / 9' }} src="https://www.loom.com/embed/9d1e8ad5bd8f4a43a5c0bc500d94e2fe" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen />

## Overview

Testing outbound calls involves two main steps:

1. Getting the list of evaluators with phone numbers
2. Running the selected evaluators for outbound testing

<Note>
  **Phone number regional coverage:** Cekura provides testing phone numbers in a limited set of regions. If you need a receiving number in a region Cekura doesn't cover (for example, an Indian +91 number), you can import your own number from [Plivo](/documentation/key-concepts/phone-numbers/plivo) or [Twilio](/documentation/key-concepts/phone-numbers/twilio). Imported numbers work identically to Cekura-provisioned numbers for inbound and outbound testing — your system calls the imported number and Cekura's testing agent answers.
</Note>

## Prerequisites

* **Outbound mode enabled**: Agent's `inbound` must be set to `false`
* **Agent `contact_number` set to your outbound caller ID**: Cekura validates the caller ID (ANI) of every incoming outbound-test call against the agent's `contact_number` field. If your agent dials from `+15551234567`, the agent's `contact_number` must be exactly `+15551234567` in E.164 format. Set this in Agent settings or via the [Update Agent API](/api-reference/test_framework/update-agent-partial).
* **Evaluators ready**: Created evaluators with valid phone numbers

<Warning>
  **Caller ID mismatch causes silent rejects.** If the `contact_number` on your agent is empty or doesn't match the number your agent dials from, Cekura drops the incoming call without returning a SIP error your telephony provider can surface. The symptom on the provider side (Twilio, Telnyx, etc.) is `Busy`, `NoAnswer`, or a generic failed dial — no audio, no answer, no descriptive error. Confirm `contact_number` matches your actual outbound caller ID before debugging anything further downstream.
</Warning>

## Getting Evaluators and Phone Numbers

To retrieve the available evaluators and their associated phone numbers:

1. Use the List Evaluators API endpoint
2. The response will include both evaluators and their corresponding phone numbers for testing

<Card title="API Reference" icon="link">
  Check out the [List Evaluators API](/api-reference/test_framework/get-test_frameworkv1scenarios-external) documentation
</Card>

### Example Code Running Outbound Call

```python theme={null}
import requests
import time
from typing import Callable, Dict, List

# Set variables
API_KEY = "api_key" # change_me
SCENARIO_IDS = [1514, 1515] # change_me
AGENT_ID = 26 # change_me


def call(number: str, test_profile: Dict | None) -> None:
    '''
    Outbound call function to be implemented
    Example: number = "+14154133900"
    '''
    print(f"📞 Initiating outbound call to {number}...")  # Replace with actual call logic


headers = {
    'X-CEKURA-API-KEY': API_KEY,
    'Content-Type': 'application/json'
}

base_url = "https://api.cekura.ai/test_framework"


def check_agent_inbound(agent_id: int) -> bool:
    url = f'{base_url}/v1/aiagents/{agent_id}/'
    data = {
        'agent_id': agent_id
    }
    response = requests.get(url, headers=headers, json=data)

    if response.status_code == 200:
        return response.json()["inbound"]
    elif response.status_code == 404:
        print(f"❌ Agent {agent_id} not found")
        exit(0)
    else:
        print(f"❌ Error checking agent inbound status: {response.status_code}")
        exit(0)

def run_evaluators(scenarios: List[int], agent_id: int, freq: int = 1) -> Dict:
    
    url = f'{base_url}/v1/scenarios/run_scenarios/'

    data = {
        'scenarios': scenarios,
        'freq': freq,
        'agent_id': agent_id
    }

    response = requests.post(url, headers=headers, json=data)

    return response.json()

status_message = {
    "completed": "✅ Run completed successfully",
    "failed": "❌ Run failed during execution",
    "pending": "⏳ Run is waiting to be executed",
    "in_progress": "🔄 Run is in progress",
    "evaluating": "🔍 Run is being evaluated",
    "in_queue": "📋 Run is waiting in queue",
    "timeout": "⏱️ Run timed out after exceeding time limit"
}

def monitor_runs(response_json: Dict, func: Callable, call_func: bool = False) -> None:

    run_ids = [str(run["id"]) for run in response_json.get("runs")]
    test_profiles = { run["id"]: run["test_profile_data"] for run in response_json.get("runs")}

    while run_ids:

        url = f'{base_url}/v1/runs/bulk/?run_ids={",".join(run_ids)}'

        runs_list = requests.get(url, headers=headers).json()

        for run in runs_list:
            phone_number = run.get("outbound_number")
            if run["status"] == "pending" and call_func:
                func(phone_number, test_profiles[run["id"]])  # Call function immediately
                run_ids.remove(str(run["id"]))
            
            if not call_func:
                print(f"{status_message.get(run['status'])} - Run ID: {run['id']}, Phone: {phone_number}")

            if run["status"] in ["completed", "failed", "timeout"]:
                run_ids.remove(str(run["id"]))
        
        if not run_ids:
            break

        time.sleep(3)  # Poll every 3 seconds


if check_agent_inbound(AGENT_ID):
    print(f"⚠️ Please change Agent {AGENT_ID} Inbound to False before running this script")
    exit(0)

response_json = run_evaluators(SCENARIO_IDS, AGENT_ID)
monitor_runs(response_json, func=call, call_func=True)
monitor_runs(response_json, func=call, call_func=False)

```

### Example Response Structure

```json theme={null}
{
  "count": 20,
  "next": "https://api.cekura.ai/test_framework/v1/scenarios-external/?agent_id=4&page=2",
  "previous": null,
  "results": [
    {
      "id": 201,
      "name": "Hindi Elderly Telephonic Inquiry",
      "personality_name": "Hindi, Old, Hearing Issue",
      "phone_number": "+14159976447"
    }
    // Additional evaluators ...
  ]
}
```

## Running Outbound Call Tests

Once you have your list of evaluators and phone numbers:

1. Select which evaluators you want to run
2. Execute the evaluators using the Running Evaluators API
3. The system will automatically make outbound calls to the phone numbers associated with the selected evaluators
4. Monitor the test results through the dashboard

<Card title="API Reference" icon="link">
  Learn more about the [Running Evaluators API](/api-reference/test_framework/post-test_frameworkv1scenarios-externalrun_scenarios)
</Card>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Calls show Busy / NoAnswer on your telephony provider but never reach Cekura">
    Cekura rejects incoming outbound-test calls whose caller ID doesn't match a configured `contact_number` on any agent in your project. Open the agent under test and confirm its `contact_number` is set to the exact E.164 number your agent dials from. An empty or mismatched `contact_number` causes every outbound attempt to be silently dropped — the symptom on your provider side is `Busy` / `NoAnswer` / no-pickup, with no audio and no SIP error explaining why. After setting the correct `contact_number`, trigger a new run; existing runs that hit this state will remain in `timeout` and cannot be recovered.
  </Accordion>

  <Accordion title="Run status stays `timeout` with no transcript">
    A `timeout` with `duration: 0` and an empty transcript means the call never connected to Cekura. Work through these in order:

    1. **Caller-ID match** — see the entry above; this is the most common cause.
    2. **Dial actually happened** — check your telephony provider's logs to confirm the outbound call was placed.
    3. **Destination number** — confirm the number your agent dialed matches the `outbound_number` returned for that run in the bulk runs API response.
  </Accordion>

  <Accordion title="Requested run count exceeds the project's concurrent call limit">
    When you trigger N outbound runs but your project's parallel call limit is M (where M \< N), Cekura returns phone numbers for **all N runs** upfront. Runs beyond the concurrent limit are queued — they remain in a non-ready state until a concurrent slot opens, at which point they transition to `pending` status. Do not dial a number before its run reaches `pending`; the call will not be matched.

    The `monitor_runs` polling loop in the example code above handles this automatically: it polls all run IDs and calls only when a run's `status` is `"pending"`.

    To configure your project's parallel call limit, see [Managing concurrent evaluator runs](/documentation/FAQ#how-can-i-limit-the-number-of-concurrent-evaluator-runs-to-manage-parallel-call-capacity) in the FAQ.
  </Accordion>
</AccordionGroup>
