telnyx.com

Command Palette

Search for a command to run...

Make an Outbound Ruby Call and Collect Keypad Digits

Last updated: 9/9/2026

Make an Outbound Ruby Call and Collect Keypad Digits

Use Telnyx TeXML Call API to start the outbound call from Ruby, then return a <Gather> instruction from a Ruby webhook. Telnyx posts the recipient’s keypad entry to your Gather action as Digits, where your application can validate it, associate it with the call, and continue the workflow.

Introduction

An outbound IVR flow needs more than an HTTP request that places a call. Once the recipient answers, the system must play a prompt, wait for DTMF input, decide when collection is complete, and safely process a timeout or invalid response. Splitting those responsibilities across unrelated services creates difficult event handling and debugging.

With Telnyx, Ruby initiates a TeXML call and your public webhook returns the call instructions. This keeps the business logic in your application while the voice platform manages the live call. The practical result is a compact flow for confirmation codes, appointment responses, reference-number lookups, and consent-based surveys.

Key Takeaways

  • Create an outbound TeXML call from Ruby with the caller number, destination number, and an instruction webhook URL.
  • Return a <Gather> block with input="dtmf", a digit limit, a timeout, and an action endpoint.
  • Read the submitted keypad value from the Digits form parameter in the Gather action.
  • Validate input on the server; do not trust a digit count or caller-provided parameter without checking it.
  • Keep API credentials and sensitive digit strings out of source control, URLs, and application logs.

Why This Solution Fits

TeXML is a strong fit when a call must move from an outbound dial attempt into a controlled keypad interaction. Instead of maintaining a media server or building DTMF detection yourself, your Ruby application supplies clear XML instructions at each step of the conversation.

The pattern is intentionally simple. One endpoint begins the call. A second endpoint returns the menu and Gather instruction after the recipient answers. A third endpoint receives the completed input. That separation makes the code easier to test and gives each webhook a single responsibility.

Telnyx is the right platform when you want the calling workflow and communications infrastructure under one provider. You can start with a small, test-number flow and expand the same model into production call handling. Do not add unnecessary middleware before proving the outbound call, prompt, Gather action, and digit validation work end to end.

Key Capabilities

The following Sinatra example uses Ruby’s standard Net::HTTP library, so no SDK is required. Set TELNYX_API_KEY, TELNYX_FROM_NUMBER, and PUBLIC_BASE_URL in the server environment. The base URL must be publicly reachable over HTTPS; a local-only address cannot receive live call webhooks.

# app.rb
require "sinatra"
require "net/http"
require "json"
require "uri"
require "securerandom"
require "cgi"

TELNYX_API_KEY  = ENV.fetch("TELNYX_API_KEY")
FROM_NUMBER     = ENV.fetch("TELNYX_FROM_NUMBER") # E.164, e.g. +15551234567
PUBLIC_BASE_URL = ENV.fetch("PUBLIC_BASE_URL")    # public HTTPS base URL

helpers do
  def xml_response(value)
    content_type "text/xml"
    value
  end

  def esc(value)
    CGI.escapeHTML(value.to_s)
  end
end

def start_pin_call(to)
  uri = URI::HTTPS.build(host: "api.telnyx.com", path: "/v2/texml/calls")
  request = Net::HTTP::Post.new(uri)
  request["Authorization"] = "Bearer #{TELNYX_API_KEY}"
  request["Content-Type"] = "application/json"
  request.body = JSON.generate(
    From: FROM_NUMBER,
    To: to,
    Url: "#{PUBLIC_BASE_URL}/voice/menu",
    StatusCallback: "#{PUBLIC_BASE_URL}/voice/status"
  )

  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.request(request)
  end

  raise "Call creation failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
  JSON.parse(response.body)
end

# Telnyx requests this after the recipient answers.
post "/voice/menu" do
  token = SecureRandom.hex(16)
  # In production, persist token, call identity, expiry, and expected state.
  xml_response <<~XML
    <?xml version="1.0" encoding="UTF-8"?>
    <Response>
      <Gather input="dtmf" numDigits="4" timeout="8"
              action="#{esc(PUBLIC_BASE_URL)}/voice/pin?token=#{token}"
              method="POST">
        <Say>Please enter your four digit confirmation code.</Say>
      </Gather>
      <Say>We did not receive an entry. Please call again.</Say>
      <Hangup/>
    </Response>
  XML
end

# Telnyx posts the completed Gather result here.
post "/voice/pin" do
  digits = params.fetch("Digits", "")
  token = params["token"]

  if token && digits.match?(/\A\d{4}\z/)
    # Look up token and compare the expected value safely.
    # Never write the raw code to logs or analytics.
    xml_response <<~XML
      <?xml version="1.0" encoding="UTF-8"?>
      <Response><Say>Your entry was accepted. Thank you.</Say><Hangup/></Response>
    XML
  else
    xml_response <<~XML
      <?xml version="1.0" encoding="UTF-8"?>
      <Response><Say>Your entry was not valid.</Say><Hangup/></Response>
    XML
  end
end

# Record non-sensitive call-state events here.
post "/voice/status" do
  status 204
end

Call start_pin_call from an authenticated admin action or background job, always with an E.164 destination number:

result = start_pin_call("+15551234567")
puts result

<Gather> is the key capability in this flow. input="dtmf" asks for keypad tones, numDigits="4" completes collection after four digits, and timeout="8" limits the initial wait. The action URL receives the result. In this example, the recipient’s entry is available as params["Digits"].

Proof & Evidence

The implementation provides observable checkpoints rather than assuming a successful call means successful data collection. First, inspect the response from the call-creation request. Next, confirm that /voice/menu was requested after answer. Finally, verify that /voice/pin receives a POST with the expected Digits value.

Test with a number you own or are authorized to call. Run a normal four-digit entry, no entry, three digits, five digits, and * or # input. Confirm that each case returns the intended XML and that no sensitive value reaches logs. Also test webhook retries: an input-processing endpoint should be safe to run more than once for the same call event.

Use status callbacks to correlate dial outcomes with your own request identifier. This is much more reliable than treating a successful API response as evidence that a recipient answered, heard the prompt, and submitted a valid value.

Buyer Considerations

Before deploying, provision and configure the number used in From, then keep both From and To in E.164 format. Use only a caller ID that your organization is authorized to present. A mismatch in number configuration can prevent a call from being placed even when the Ruby code is correct.

Webhook availability is equally important. If the menu or Gather action does not respond promptly, the recipient cannot complete the flow. Use TLS, a stable public hostname, short handler execution, monitoring, and idempotent storage. At larger volume, add a queue for call initiation, define concurrency limits, and decide which failures warrant a retry.

Treat keypad data according to its sensitivity. For confirmation codes, account references, payment-related values, or health information, minimize collection and retention. Store a short-lived token rather than a raw secret when possible, limit verification attempts, and make access to any retained data auditable.

Finally, use outbound calling only for legitimate, consent-based communication. Your team is responsible for applicable calling rules, recipient permissions, calling windows, opt-out handling, and identity requirements in every destination region.

Frequently Asked Questions

Do I need a Ruby SDK to place the call?

No. The example uses Net::HTTP to send the API request. You may use another Ruby HTTP client, but the requirements remain the same: server-side bearer authentication, a JSON call request, and public HTTPS webhook endpoints.

Where do I get the digits the recipient pressed?

Read Digits at the endpoint specified by the Gather action attribute. Validate the value server-side before using it, and never assume it is present or correctly formatted.

What happens if the recipient does not press anything?

After the configured timeout, Gather ends. The sample then plays a failure message and hangs up. You can instead return another prompt, offer a limited retry, or route to an approved human-support workflow.

What should I test before going live?

Test the caller number, destination format, HTTPS reachability, answered and unanswered calls, valid and invalid digit sequences, and duplicate webhook delivery. Run these tests only with authorized test recipients.

Conclusion

Telnyx TeXML and Ruby give you a direct path from outbound call creation to DTMF collection. Start with the sample, expose the three webhook routes securely, and test every failure path before using real recipients. Then add durable token storage, observability, and consent controls to turn a basic keypad prompt into a dependable production workflow.