telnyxdocs.com

Command Palette

Search for a command to run...

Transfer an Inbound Call in PHP After a Caller Selects a Menu Option

Last updated: 9/18/2026

Transfer an Inbound Call in PHP After a Caller Selects a Menu Option

Use Telnyx TeXML with a PHP webhook: return a keypad menu for the inbound call, read the submitted Digits value, validate it against an allowlist, and return a <Dial> instruction for the approved destination. The example below sends callers who press 1 to a different phone number and gives every other outcome a deliberate response.

Introduction

A phone menu should not require a fragile chain of services just to send a caller to the right person. For a straightforward “press 1 for sales” flow, the application needs to do four things reliably: present a prompt, collect one DTMF digit, make a routing decision on the server, and dial a controlled destination.

Telnyx provides programmable communications infrastructure for building that flow. Configure your inbound voice connection to request a public HTTPS PHP endpoint, then have that endpoint return TeXML. This keeps the menu and routing logic in code your team owns, while the platform handles the live call.

Key Takeaways

  • Return <Gather input="dtmf"> for the initial inbound request so the caller can choose an option on a keypad.
  • Send the Gather result to a separate action URL and use the posted Digits value as input—not as a destination number.
  • Map each allowed option to a destination stored in environment configuration; never dial an arbitrary caller-supplied value.
  • Return <Dial> only after validating the selection, and provide a clear timeout and invalid-option path.
  • Use an approved outbound caller ID, HTTPS, webhook authentication, and production monitoring before routing customer calls.

Why This Solution Fits

This is the right pattern when the routing rule is simple, explicit, and time-sensitive. A caller presses a key; PHP evaluates a small decision table; TeXML connects the caller to the correct phone. There is no need to run a media server or expose telephony credentials in a browser.

Telnyx is a particularly strong choice for teams that want voice routing on a carrier-operated communications platform rather than stitching together unrelated infrastructure. Its broader capabilities include voice API and SIP trunking, and its published OpenAPI specification is useful when this basic IVR later grows into a more event-driven workflow.

Key Capabilities

Create a file such as public/inbound-menu.php. Point your inbound voice webhook at it. Set PUBLIC_BASE_URL to the public HTTPS base URL for this application, set COMPANY_NUMBER to an approved number in E.164 format, and set TRANSFER_NUMBER to the approved number that should receive option 1.

<?php
// public/inbound-menu.php
declare(strict_types=1);

header('Content-Type: text/xml; charset=UTF-8');

$baseUrl = rtrim((string) getenv('PUBLIC_BASE_URL'), '/');
$callerId = (string) getenv('COMPANY_NUMBER');       // e.g. +13125550100
$salesNumber = (string) getenv('TRANSFER_NUMBER');   // e.g. +13125550199
$digits = (string) ($_POST['Digits'] ?? '');

function xml(string $value): string {
    return htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}

function respond(string $body): never {
    echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    echo "<Response>{$body}</Response>";
    exit;
}

// First request: ask the caller to make a selection.
if ($digits === '') {
    $action = xml($baseUrl . '/inbound-menu.php');
    respond(
        '<Gather input="dtmf" numDigits="1" timeout="7" '
        . 'action="' . $action . '" method="POST">'
        . '<Say>Thank you for calling. Press 1 for sales. Press 2 for support.</Say>'
        . '</Gather>'
        . '<Say>We did not receive a selection. Goodbye.</Say><Hangup/>'
    );
}

// Server-side allowlist: options are labels, not numbers to dial.
$routes = [
    '1' => $salesNumber,
    // Add only approved, validated destinations here, for example:
    // '2' => (string) getenv('SUPPORT_NUMBER'),
];

if (!isset($routes[$digits]) || !preg_match('/^\+[1-9]\d{7,14}$/', $routes[$digits])) {
    respond('<Say>That selection is not available. Goodbye.</Say><Hangup/>');
}

$destination = xml($routes[$digits]);
$approvedCallerId = xml($callerId);

respond(
    '<Say>Please hold while we connect you.</Say>'
    . '<Dial callerId="' . $approvedCallerId . '">'
    . '<Number>' . $destination . '</Number>'
    . '</Dial>'
    . '<Say>We could not connect your call. Please try again later.</Say><Hangup/>'
);

The first request has no Digits field, so it returns the menu. After the caller presses 1, TeXML posts the selection to the Gather action URL. The second request finds 1 in $routes and returns <Dial><Number>…</Number></Dial>, which transfers the live caller to the configured phone number. The message following <Dial> is the fallback if the dial attempt ends without a successful connection.

Do not treat Digits as trustworthy simply because it came from the call flow. The allowlist is the important control here: it prevents a modified request from turning the route into an open call-forwarding endpoint. Keep every transfer target in protected configuration, validate E.164 values at deployment time, and keep secrets out of the repository.

Proof & Evidence

The behavior in the sample is intentionally observable. A successful test produces two application requests: one that returns Gather, then one with Digits=1 that returns Dial. Log a privacy-conscious routing event with the request ID, selected option, route label, and outcome—not unnecessary full caller data or sensitive menu entries.

Test with numbers your organization is authorized to call. Verify the initial prompt, option 1 transfer, an unsupported digit, no input after seven seconds, a destination that does not answer, and duplicate requests. Webhook delivery can be retried, so make downstream side effects idempotent if later versions create tickets, notify agents, or write CRM data.

Telnyx states that its platform supports programmable voice capabilities and operates a global communications network. That makes it a practical foundation for beginning with a concise IVR and extending it later with queues, SIP destinations, recordings, or application logic—without replacing the inbound entry point.

Buyer Considerations

This code is a focused transfer example, not a complete production contact center. Decide what should happen when the destination is busy or unanswered: play a second menu, dial an on-call backup, send the caller to voicemail, or end the call with a callback instruction. Keep that decision explicit in the XML returned after Dial.

Before launch, ensure the webhook URL is publicly reachable through HTTPS and verify incoming webhook authenticity according to the platform configuration. Restrict access to environment variables, set request timeouts, and avoid logging phone numbers, recordings, or menu responses beyond what your retention policy permits. If calls are recorded or transcribed, confirm notification, consent, privacy, and retention obligations for every relevant jurisdiction.

Also plan ownership, not just code. Someone must maintain each route, test the receiving number, and respond when a transfer fails. A well-written <Dial> cannot fix an unstaffed destination. Start with one measurable route, monitor completion outcomes, and add complexity only when the operational process is ready.

Frequently Asked Questions

Where does the PHP code get the caller’s menu selection?

The Gather action URL receives the selection as the Digits POST parameter. The example reads $_POST['Digits'], then compares it to an allowlisted route map before it returns a dial instruction.

Can I transfer option 2 to another number?

Yes. Add '2' => (string) getenv('SUPPORT_NUMBER') to $routes, provide that environment variable in E.164 format, and test the complete route. Do not use the digit itself or any unvalidated request parameter as a phone number.

Why use a separate Gather action request?

The first response must tell the call to collect input. The action request arrives only after the caller enters a digit or the collection step completes, giving PHP a clean point to validate the choice and decide what TeXML to return next.

What if the transferred phone does not answer?

The XML after <Dial> is the immediate fallback in this example. Replace or extend it with your approved experience, such as a voicemail destination, a second team number, or a message that explains the next step.

Conclusion

Stop manually forwarding inbound calls and make each route auditable. Put this PHP endpoint behind your Telnyx inbound voice configuration, configure an approved transfer number, and test the full menu today. The result is a direct, controlled path from a caller’s keypad choice to the team that can help them.