# Telnyx Numbers: Identity — Full Documentation > Complete page content for Identity (Numbers section) of the Telnyx developer docs (https://developers.telnyx.com). > Root index: https://developers.telnyx.com/llms.txt · Lightweight index for this subsection: https://telnyx.com/llms/numbers/identity.txt ## Number Lookup ### Number lookup using Telnyx APIs > Source: https://developers.telnyx.com/docs/identity/number-lookup/quickstart.md ## Lookup: Carrier and caller name | [cURL](#curl) | [Python](#python) | [Node](#node) | [Ruby](#ruby) | ----- ## cURL Number Lookup is a service offered by the Telnyx SDK that allows the user to look up a phone number and retrieve information regarding that phone number such as: - Carrier name - Type of phone (landline, mobile, etc.) - Name and business associated with the number ### Basic number lookup example Running the following code will perform a number lookup on the given number parameter. *Don't forget to update `YOUR_API_KEY` here.* ```bash curl -X GET \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --globoff "https://api.telnyx.com/v2/number_lookup/+18665552368" ``` After pasting the above content, Kindly check and remove any new line added The response will look something like this: ```json { "data": { "caller_name": { "caller_name": "TELNYX LLC", "error_code": "10001" }, "carrier": { "error_code": null, "mobile_country_code": "US", "mobile_network_code": 866, "name": "Telnyx/4", "type": "voip" }, "country_code": "US", "fraud": null, "national_format": "(0312) 945-7420", "phone_number": "+13129457420", "portability": { "altspid": "073H", "altspid_carrier_name": "Telnyx/4", "altspid_carrier_type": "3", "city": "WAUKEGAN", "line_type": "voip", "lrn": "2245701999", "ocn": "073H", "ported_date": "2017-10-20", "ported_status": "Y", "spid": "073H", "spid_carrier_name": "Telnyx/4", "spid_carrier_type": "3", "state": "Illinois" }, "record_type": "number_lookup" } } ``` After pasting the above content, Kindly check and remove any new line added This example shows results for a Telnyx user-owned number. It displays the formatted version of the number in the national-format value. The location of the number can be seen under portability -> city and state. Other useful information such as carrier and caller_name can be retrieved if available. ### Retrieving caller name and carrier Users may also specify the type of the request in order to receive information about the carrier and caller-name. Note that the request will always return the same object, however if the caller-name and carrier options are not sent, the return values will be null. An example of adding both these parameters to the request is shown below: ```bash curl -X GET \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --globoff "https://api.telnyx.com/v2/number_lookup/+18665552368?carrier&caller-name" ``` After pasting the above content, Kindly check and remove any new line added ## Python Number Lookup is a serviceis a service offered by the Telnyx SDK that allows the user to look up a phone number and retrieve information regarding that phone number such as: - Carrier name - Type of phone (landline, mobile, etc.) - Name and business associated with the number ### Basic number lookup example Running the following code will perform a number lookup on the given number parameter using Python. ```python import telnyx telnyx.api_key = "YOUR_API_KEY" telnyx.NumberLookup.retrieve(“+13129457420”) ``` After pasting the above content, Kindly check and remove any new line added The response will look something like this: ```json { "data": { "caller_name": { "caller_name": "TELNYX LLC", "error_code": "10001" }, "carrier": { "error_code": null, "mobile_country_code": "US", "mobile_network_code": 866, "name": "Telnyx/4", "type": "voip" }, "country_code": "US", "fraud": null, "national_format": "(0312) 945-7420", "phone_number": "+13129457420", "portability": { "altspid": "073H", "altspid_carrier_name": "Telnyx/4", "altspid_carrier_type": "3", "city": "WAUKEGAN", "line_type": "voip", "lrn": "2245701999", "ocn": "073H", "ported_date": "2017-10-20", "ported_status": "Y", "spid": "073H", "spid_carrier_name": "Telnyx/4", "spid_carrier_type": "3", "state": "Illinois" }, "record_type": "number_lookup" } } ``` After pasting the above content, Kindly check and remove any new line added This example shows results for a Telnyx user-owned number. It displays the formatted version of the number in the national-format value. The location of the number can be seen under portability -> city and state. ### Retrieving caller name and carrier Users may also specify the type of the request in order to receive information about the carrier and caller-name. Note that the request will always return the same object, however if the caller-name and carrier options are not sent, the return values will be null. ## Node Number Lookup is a serviceoffered by the Telnyx SDK that allows the user to look up a phone number and retrieve information regarding that phone number such as: - Carrier name - Type of phone (landline, mobile, etc.) - Name and business associated with the number ### Basic number lookup example Running the following code will perform a number lookup on the given number parameter. ```javascript import Telnyx from 'telnyx'; const telnyx = new Telnyx("YOUR_API_KEY"); // In Node 10 const { data: numberInfo } = await telnyx.numberLookup.retrieve( '+18665552368' ); // In other environments telnyx.numberLookup.retrieve( '+18665552368' ).then(function(response){ const numberInfo = response.data; // asynchronously handled }); ``` After pasting the above content, Kindly check and remove any new line added The response will look something like this: ```json { "data": { "caller_name": { "caller_name": "TELNYX LLC", "error_code": "10001" }, "carrier": { "error_code": null, "mobile_country_code": "US", "mobile_network_code": 866, "name": "Telnyx/4", "type": "voip" }, "country_code": "US", "fraud": null, "national_format": "(0312) 945-7420", "phone_number": "+13129457420", "portability": { "altspid": "073H", "altspid_carrier_name": "Telnyx/4", "altspid_carrier_type": "3", "city": "WAUKEGAN", "line_type": "voip", "lrn": "2245701999", "ocn": "073H", "ported_date": "2017-10-20", "ported_status": "Y", "spid": "073H", "spid_carrier_name": "Telnyx/4", "spid_carrier_type": "3", "state": "Illinois" }, "record_type": "number_lookup" } } ``` After pasting the above content, Kindly check and remove any new line added This example shows results for a Telnyx user-owned number. It displays the formatted version of the number in the national-format value. The location of the number can be seen under portability -> city and state. Other useful information such as carrier and caller_name can be retrieved if available. ### Retrieving caller name and carrier Users may also specify the type of the request in order to receive information about the carrier and caller-name. Note that the request will always return the same object, however if the caller-name and carrier options are not sent, the return values will be null. An example request for these options is shown below: ```javascript import Telnyx from 'telnyx'; const telnyx = new Telnyx("YOUR_API_KEY"); // In Node 10 const { data: numberInfo } = await telnyx.numberLookup.retrieve( '+18665552368', { type: ["caller-name", "carrier"] } ); // In other environments telnyx.numberLookup.retrieve( '+18665552368', { type: ["caller-name", "carrier"] } ).then(function(response){ const numberInfo = response.data; // asynchronously handled }); ``` After pasting the above content, Kindly check and remove any new line added ## Ruby Number Lookup is a service is a service offered by the Telnyx SDK that allows the user to look up a phone number and retrieve information regarding that phone number such as: - Carrier name - Type of phone (landline, mobile, etc.) - Name and business associated with the number ### Basic number lookup example Running the following code will perform a number lookup on the given number parameter. ```ruby require "telnyx" Telnyx.api_key = "YOUR_API_KEY" Telnyx::NumberLookup.retrieve('+12624755500') ``` After pasting the above content, Kindly check and remove any new line added The response will look something like this: ```json { "data": { "caller_name": { "caller_name": "TELNYX LLC", "error_code": "10001" }, "carrier": { "error_code": null, "mobile_country_code": "US", "mobile_network_code": 866, "name": "Telnyx/4", "type": "voip" }, "country_code": "US", "fraud": null, "national_format": "(0312) 945-7420", "phone_number": "+13129457420", "portability": { "altspid": "073H", "altspid_carrier_name": "Telnyx/4", "altspid_carrier_type": "3", "city": "WAUKEGAN", "line_type": "voip", "lrn": "2245701999", "ocn": "073H", "ported_date": "2017-10-20", "ported_status": "Y", "spid": "073H", "spid_carrier_name": "Telnyx/4", "spid_carrier_type": "3", "state": "Illinois" }, "record_type": "number_lookup" } } ``` After pasting the above content, Kindly check and remove any new line added This example shows results for a Telnyx user-owned number. It displays the formatted version of the number in the national-format value. The location of the number can be seen under portability -> city and state. Other useful information such as carrier and caller_name can be retrieved if available. ### Retrieving caller name and carrier Users may also specify the type of the request in order to receive information about the carrier and caller-name. Note that the request will always return the same object, however if the caller-name and carrier options are not sent, the return values will be null. --- ## Verify ### Telnyx verify quickstart > Source: https://developers.telnyx.com/docs/identity/verify/quickstart.md In this tutorial, you'll learn how to deliver a 2FA token to any mobile number and verify that token using the Telnyx v2 API. Examples are provided in **curl**, **Node.js**, **Python**, **Ruby**, **Go**, **Java**, **.NET**, and **PHP**. - - - ## Prerequisites Sign up at [telnyx.com](https://telnyx.com/sign-up) if you don't have an account yet. Follow the [API Keys guide](/development/api-fundamentals/create-api-keys) to generate an API key. Set it as an environment variable: ```bash macOS/Linux export TELNYX_API_KEY="YOUR_API_KEY" ``` ```powershell Windows $env:TELNYX_API_KEY = "YOUR_API_KEY" ``` If you prefer using an SDK over curl, install one for your language: ```bash Node.js npm install telnyx ``` ```bash Python pip install telnyx ``` ```bash Ruby gem install telnyx ``` ```bash Go go get github.com/team-telnyx/telnyx-go ``` ```bash PHP composer require telnyx/telnyx-php ``` For **Java** and **.NET**, the examples use the standard HTTP client libraries included with the platform. ## Methods of verification There are currently three verification methods available: * **`sms`** - the verification code is sent in a custom or default templated message. * **`call`** - the code is spoken aloud in a custom or default templated message when the user answers the call. * **`flashcall`** - the verification code is embedded in the caller ID of a brief "flash" call (the call rings once and hangs up). The user's app extracts the code automatically. - - - ## Create a verify profile A Verify Profile contains several important configurations that you'll use when sending 2-factor authentication messages and receiving responses. Before you send any 2FA messages, you need a profile to go with them. Each profile can have one of each verification method configured. It is recommended that if you wish to configure multiple applications, you use a different profile for each one. In the below example we will set up a verification profile that can use SMS using a selected message template and speech to text calling. ### Select a message template ```bash curl curl --location --request GET 'https://api.telnyx.com/v2/verify_profiles/templates' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const templates = await client.verifyProfiles.retrieveTemplates(); for (const template of templates.data) { console.log(`${template.id}: ${template.text}`); } ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) templates = client.verify_profiles.retrieve_templates() for template in templates.data: print(f"{template.id}: {template.text}") ``` ```ruby Ruby require "telnyx" require "json" require "net/http" require "uri" uri = URI("https://api.telnyx.com/v2/verify_profiles/templates") req = Net::HTTP::Get.new(uri) req["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } data = JSON.parse(res.body) data["data"].each do |template| puts "#{template['id']}: #{template['text']}" end ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient(option.WithAPIKey(os.Getenv("TELNYX_API_KEY"))) templates, err := client.VerifyProfiles.RetrieveTemplates(context.TODO()) if err != nil { panic(err) } for _, t := range templates.Data { fmt.Printf("%s: %s\n", t.ID, t.Text) } } ``` ```java Java package com.telnyx.example; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public final class ListTemplates { public static void main(String[] args) throws Exception { HttpClient httpClient = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.telnyx.com/v2/verify_profiles/templates")) .header("Authorization", "Bearer " + System.getenv("TELNYX_API_KEY")) .GET() .build(); HttpResponse response = httpClient.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println(response.body()); } } ``` ```csharp .NET using System.Net.Http.Headers; var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var response = await httpClient.GetStringAsync( "https://api.telnyx.com/v2/verify_profiles/templates" ); Console.WriteLine(response); ``` ```php PHP true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer $apiKey", ], ]); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); foreach ($data['data'] as $template) { echo "{$template['id']}: {$template['text']}\n"; } ``` ### Example response ```json { "data": [ { "id": "0abb5b4f-459f-445a-bfcd-488998b7572d", "text": "Your {{app_name}} verification code is: {{code}}." }, { "id": "2ca3f1da-5621-4aa6-ae56-df21caff79e0", "text": "{{code}} is your verification code for {{app_name}}." }, { "id": "33dfb056-6c1b-40bd-920e-4243e01248a5", "text": "Your {{app_name}} verification code is: {{code}}. Do not share this code with anyone; our employees will never ask for the code." }, { "id": "46acd63c-be57-4993-ae8d-0e4067ad1d57", "text": "Your {{app_name}} verification code is: {{code}}. This code will expire in {{default_verification_timeout_secs}} minutes." }, { "id": "723ead5e-ada6-4c29-a962-349170866187", "text": "{{code}} is your verification code for {{app_name}}. This code will expire in {{default_verification_timeout_secs}} minutes." }, { "id": "88d0781f-f4c7-4b78-8d0a-1f3e4de78b5e", "text": "Your {{app_name}} verification code is: {{code}}. This code will expire in {{default_verification_timeout_secs}} minutes. Do not share this code with anyone; our employees will never ask for the code." } ] } ``` If not selected then the default template is "Your verification code is `{code}`." Want to use your own branded verification messages? You can now [create custom templates](/docs/identity/verify/custom-templates) that match your brand voice and compliance requirements. ### Create a verify profile ```bash curl curl --location --request POST 'https://api.telnyx.com/v2/verify_profiles' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data-raw '{ "name": "foobar-en-v1", "language": "en-US", "sms": { "messaging_template_id": "0abb5b4f-459f-445a-bfcd-488998b7572d", "whitelisted_destinations": ["US", "CA"], "default_timeout_secs": 300, "code_length": 5 }, "call": { "default_timeout_secs": 600 } }' ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const profile = await client.verifyProfiles.create({ name: 'foobar-en-v1', language: 'en-US', sms: { messaging_template_id: '0abb5b4f-459f-445a-bfcd-488998b7572d', whitelisted_destinations: ['US', 'CA'], default_timeout_secs: 300, code_length: 5, }, call: { default_timeout_secs: 600, }, }); console.log('Profile ID:', profile.data.id); console.log('Profile name:', profile.data.name); ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) profile = client.verify_profiles.create( name="foobar-en-v1", language="en-US", sms={ "messaging_template_id": "0abb5b4f-459f-445a-bfcd-488998b7572d", "whitelisted_destinations": ["US", "CA"], "default_timeout_secs": 300, "code_length": 5, }, call={ "default_timeout_secs": 600, }, ) print(f"Profile ID: {profile.data.id}") print(f"Profile name: {profile.data.name}") ``` ```ruby Ruby require "telnyx" require "json" require "net/http" require "uri" uri = URI("https://api.telnyx.com/v2/verify_profiles") req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json") req["Authorization"] = "Bearer #{ENV['TELNYX_API_KEY']}" req.body = { name: "foobar-en-v1", language: "en-US", sms: { messaging_template_id: "0abb5b4f-459f-445a-bfcd-488998b7572d", whitelisted_destinations: ["US", "CA"], default_timeout_secs: 300, code_length: 5 }, call: { default_timeout_secs: 600 } }.to_json res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } data = JSON.parse(res.body) puts "Profile ID: #{data['data']['id']}" puts "Profile name: #{data['data']['name']}" ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient(option.WithAPIKey(os.Getenv("TELNYX_API_KEY"))) profile, err := client.VerifyProfiles.Create(context.TODO(), telnyx.VerifyProfileCreateParams{ Name: "foobar-en-v1", Language: telnyx.String("en-US"), SMS: &telnyx.VerifyProfileSMSParams{ MessagingTemplateID: telnyx.String("0abb5b4f-459f-445a-bfcd-488998b7572d"), WhitelistedDestinations: []string{"US", "CA"}, DefaultTimeoutSecs: telnyx.Int64(300), CodeLength: telnyx.Int64(5), }, Call: &telnyx.VerifyProfileCallParams{ DefaultTimeoutSecs: telnyx.Int64(600), }, }) if err != nil { panic(err) } fmt.Printf("Profile ID: %s\n", profile.Data.ID) fmt.Printf("Profile name: %s\n", profile.Data.Name) } ``` ```java Java package com.telnyx.example; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public final class CreateVerifyProfile { public static void main(String[] args) throws Exception { String body = """ { "name": "foobar-en-v1", "language": "en-US", "sms": { "messaging_template_id": "0abb5b4f-459f-445a-bfcd-488998b7572d", "whitelisted_destinations": ["US", "CA"], "default_timeout_secs": 300, "code_length": 5 }, "call": { "default_timeout_secs": 600 } } """; HttpClient httpClient = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.telnyx.com/v2/verify_profiles")) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + System.getenv("TELNYX_API_KEY")) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = httpClient.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println(response.body()); } } ``` ```csharp .NET using System.Net.Http.Headers; using System.Text; using System.Text.Json; var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var payload = new { name = "foobar-en-v1", language = "en-US", sms = new { messaging_template_id = "0abb5b4f-459f-445a-bfcd-488998b7572d", whitelisted_destinations = new[] { "US", "CA" }, default_timeout_secs = 300, code_length = 5 }, call = new { default_timeout_secs = 600 } }; var content = new StringContent( JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json" ); var response = await httpClient.PostAsync( "https://api.telnyx.com/v2/verify_profiles", content ); var body = await response.Content.ReadAsStringAsync(); Console.WriteLine(body); ``` ```php PHP 'foobar-en-v1', 'language' => 'en-US', 'sms' => [ 'messaging_template_id' => '0abb5b4f-459f-445a-bfcd-488998b7572d', 'whitelisted_destinations' => ['US', 'CA'], 'default_timeout_secs' => 300, 'code_length' => 5, ], 'call' => [ 'default_timeout_secs' => 600, ], ]); $ch = curl_init('https://api.telnyx.com/v2/verify_profiles'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ "Authorization: Bearer $apiKey", 'Content-Type: application/json', ], ]); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); echo "Profile ID: " . $data['data']['id'] . "\n"; echo "Profile name: " . $data['data']['name'] . "\n"; ``` ### Example response ```json { "data": { "id": "4900017a-e7c8-e79e-0a7c-0d98f49b09cc", "name": "foobar-en-v1", "created_at": "2024-03-27T11:45:41.292913", "updated_at": "2024-03-27T11:45:41.292913", "record_type": "verify_profile", "sms": { "messaging_template_id": "0abb5b4f-459f-445a-bfcd-488998b7572d", "whitelisted_destinations": ["US", "CA"], "default_timeout_secs": 300, "code_length": 5 }, "call": { "default_timeout_secs": 600 }, "language": "en-US" } } ``` Don't forget to set your `TELNYX_API_KEY` environment variable or replace `YOUR_API_KEY` in the curl examples. Take note of the profile's `id` that's returned to you, you'll need it to send 2FA verifications. At any time, you can access all of your created Verify Profiles [by API](/api-reference/verify/list-all-verify-profiles) as well. You are now ready to send 2-factor authentication messages! ## Trigger a verification request To send a verification attempt, you need the Verify Profile ID, the phone number that will receive the message, and the verification type. Choose the method that best fits your use case: SMS verification sends a code via text message. This is the most common method with the widest global coverage. ```bash curl curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{"phone_number":"+13035551234","verify_profile_id":"4900017a-e7c8-e79e-0a7c-0d98f49b09cc"}' \ https://api.telnyx.com/v2/verifications/sms ``` ```javascript Node import Telnyx from 'telnyx'; const client = new Telnyx({ apiKey: process.env['TELNYX_API_KEY'], }); const verification = await client.verifications.triggerSMS({ phone_number: '+13035551234', verify_profile_id: '4900017a-e7c8-e79e-0a7c-0d98f49b09cc' }); console.log('Verification ID:', verification.data.id); console.log('Status:', verification.data.status); // "pending" ``` ```python Python import os from telnyx import Telnyx client = Telnyx(api_key=os.environ.get("TELNYX_API_KEY")) verification = client.verifications.trigger_sms( phone_number="+13035551234", verify_profile_id="4900017a-e7c8-e79e-0a7c-0d98f49b09cc" ) print(f"Verification ID: {verification.data.id}") print(f"Status: {verification.data.status}") # "pending" ``` ```ruby Ruby require "telnyx" client = Telnyx::Client.new(api_key: ENV["TELNYX_API_KEY"]) verification = client.verifications.sms( phone_number: "+13035551234", verify_profile_id: "4900017a-e7c8-e79e-0a7c-0d98f49b09cc" ) puts "Verification ID: #{verification.id}" puts "Status: #{verification.status}" # "pending" ``` ```go Go package main import ( "context" "fmt" "os" "github.com/team-telnyx/telnyx-go" "github.com/team-telnyx/telnyx-go/option" ) func main() { client := telnyx.NewClient(option.WithAPIKey(os.Getenv("TELNYX_API_KEY"))) verification, err := client.Verifications.SMS(context.TODO(), telnyx.VerificationSMSParams{ PhoneNumber: "+13035551234", VerifyProfileID: "4900017a-e7c8-e79e-0a7c-0d98f49b09cc", }) if err != nil { panic(err) } fmt.Printf("Verification ID: %s\n", verification.Data.ID) fmt.Printf("Status: %s\n", verification.Data.Status) // "pending" } ``` ```java Java package com.telnyx.example; import com.telnyx.sdk.client.TelnyxClient; import com.telnyx.sdk.client.okhttp.TelnyxOkHttpClient; import com.telnyx.sdk.models.verify.VerificationParams; import com.telnyx.sdk.models.verify.VerificationResponse; public final class Main { public static void main(String[] args) { TelnyxClient client = TelnyxOkHttpClient.fromEnv(); VerificationParams params = VerificationParams.builder() .phoneNumber("+13035551234") .verifyProfileId("4900017a-e7c8-e79e-0a7c-0d98f49b09cc") .build(); VerificationResponse response = client.verifications().sms(params); System.out.println("Verification ID: " + response.getId()); System.out.println("Status: " + response.getStatus()); // "pending" } } ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new VerificationService(); var response = await service.SendSmsAsync(new VerificationSmsOptions { PhoneNumber = "+13035551234", VerifyProfileId = "4900017a-e7c8-e79e-0a7c-0d98f49b09cc" }); Console.WriteLine($"Verification ID: {response.Data.Id}"); Console.WriteLine($"Status: {response.Data.Status}"); // "pending" ``` ```php PHP '+13035551234', 'verify_profile_id' => '4900017a-e7c8-e79e-0a7c-0d98f49b09cc', 'type' => 'sms' ]); echo "Verification ID: " . $verification->id . "\n"; echo "Status: " . $verification->status . "\n"; // "pending" ``` **User experience:** The user receives a text message with a numeric code (e.g., "Your verification code is: 17686"). They enter this code in your application. Call verification speaks the code aloud when the user answers. Useful when the user cannot receive SMS (landlines, VoIP) or in regions with unreliable SMS delivery. **Profile configuration for call:** When creating your verify profile, include the `call` configuration: ```bash curl --location --request POST 'https://api.telnyx.com/v2/verify_profiles' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data-raw '{ "name": "my-app-call-verify", "language": "en-US", "call": { "default_timeout_secs": 600 } }' ``` **Initiate a call verification:** ```bash curl curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{"phone_number":"+13035551234","verify_profile_id":"4900017a-e7c8-e79e-0a7c-0d98f49b09cc"}' \ https://api.telnyx.com/v2/verifications/call ``` ```javascript Node const verification = await client.verifications.triggerCall({ phone_number: '+13035551234', verify_profile_id: '4900017a-e7c8-e79e-0a7c-0d98f49b09cc' }); console.log('Verification ID:', verification.data.id); console.log('Type:', verification.data.type); // "call" ``` ```python Python verification = client.verifications.trigger_call( phone_number="+13035551234", verify_profile_id="4900017a-e7c8-e79e-0a7c-0d98f49b09cc" ) print(f"Verification ID: {verification.data.id}") print(f"Type: {verification.data.type}") # "call" ``` ```ruby Ruby verification = client.verifications.call( phone_number: "+13035551234", verify_profile_id: "4900017a-e7c8-e79e-0a7c-0d98f49b09cc" ) puts "Verification ID: #{verification.id}" puts "Type: #{verification.type}" # "call" ``` ```go Go verification, err := client.Verifications.Call(context.TODO(), telnyx.VerificationCallParams{ PhoneNumber: "+13035551234", VerifyProfileID: "4900017a-e7c8-e79e-0a7c-0d98f49b09cc", }) if err != nil { panic(err) } fmt.Printf("Verification ID: %s\n", verification.Data.ID) ``` ```java Java VerificationResponse response = client.verifications().call( VerificationParams.builder() .phoneNumber("+13035551234") .verifyProfileId("4900017a-e7c8-e79e-0a7c-0d98f49b09cc") .build() ); System.out.println("Verification ID: " + response.getId()); ``` ```csharp .NET var response = await service.SendCallAsync(new VerificationCallOptions { PhoneNumber = "+13035551234", VerifyProfileId = "4900017a-e7c8-e79e-0a7c-0d98f49b09cc" }); Console.WriteLine($"Verification ID: {response.Data.Id}"); ``` ```php PHP $verification = \Telnyx\Verification::create([ 'phone_number' => '+13035551234', 'verify_profile_id' => '4900017a-e7c8-e79e-0a7c-0d98f49b09cc', 'type' => 'call' ]); echo "Verification ID: " . $verification->id . "\n"; ``` **User experience:** The user receives a phone call. An automated voice reads the verification code aloud (e.g., "Your verification code is 1-7-6-8-6"). The code is repeated twice. The user then enters the code in your application. **DTMF confirmation:** You can optionally enable DTMF confirmation, where the user enters the code on their phone keypad during the call instead of typing it in your app. See the [DTMF Confirmation guide](/docs/identity/verify/dtmf-confirm) for details. ### Example response (all methods) ```json { "data": { "created_at": "2020-09-14T17:03:32.965812", "id": "12ade33a-21c0-473b-b055-b3c836e1c292", "phone_number": "+13035551234", "record_type": "verification", "status": "pending", "timeout_secs": 300, "verify_profile_id": "4900017a-e7c8-e79e-0a7c-0d98f49b09cc", "type": "sms", "updated_at": "2020-09-14T17:03:32.965812" } } ``` ## Choosing the right verification method | Feature | SMS | Call | Flashcall | |---------|-----|------|-----------| | **Delivery speed** | 1–5 seconds | 10–20 seconds (ring + answer) | 2–5 seconds | | **User interaction** | Read code, type it in | Listen to code, type it in | Automatic (app reads caller ID) | | **Works on web** | ✅ Yes | ✅ Yes | ❌ No (mobile only) | | **Works on landlines** | ❌ No | ✅ Yes | ❌ No | | **Cost** | Per-message rate | Per-minute rate | Per-call rate (very short) | | **Global coverage** | Widest | Wide | Limited | | **Accessibility** | Good | Best (audio) | App-dependent | | **Best for** | General purpose | Landlines, accessibility | Mobile apps with auto-read | **Fallback strategy:** Configure multiple methods in your verify profile. Start with SMS (fastest and widest coverage), fall back to call if SMS fails. For mobile apps, consider flashcall as a zero-friction primary method with SMS as fallback. ## Verify a 2FA code The user provides the code they received (via SMS or call). Submit it to Telnyx to verify it matches: ```bash curl curl -X POST \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --header "Authorization: Bearer YOUR_API_KEY" \ --data '{"code":"17686", "verify_profile_id": "4900017a-e7c8-e79e-0a7c-0d98f49b09cc"}' \ https://api.telnyx.com/v2/verifications/by_phone_number/+13035551234/actions/verify ``` ```javascript Node const result = await client.verifications.byPhoneNumber.actions.verify( '+13035551234', { code: '17686', verify_profile_id: '4900017a-e7c8-e79e-0a7c-0d98f49b09cc', } ); if (result.data.response_code === 'accepted') { console.log('Verification successful!'); } else { console.log('Verification failed:', result.data.response_code); } ``` ```python Python result = client.verifications.by_phone_number.actions.verify( "+13035551234", code="17686", verify_profile_id="4900017a-e7c8-e79e-0a7c-0d98f49b09cc" ) if result.data.response_code == "accepted": print("Verification successful!") else: print(f"Verification failed: {result.data.response_code}") ``` ```ruby Ruby result = client.verifications.verify( "+13035551234", code: "17686", verify_profile_id: "4900017a-e7c8-e79e-0a7c-0d98f49b09cc" ) if result.response_code == "accepted" puts "Verification successful!" else puts "Verification failed: #{result.response_code}" end ``` ```go Go result, err := client.Verifications.Verify(context.TODO(), "+13035551234", telnyx.VerificationVerifyParams{ Code: "17686", VerifyProfileID: "4900017a-e7c8-e79e-0a7c-0d98f49b09cc", }) if err != nil { panic(err) } if result.Data.ResponseCode == "accepted" { fmt.Println("Verification successful!") } else { fmt.Printf("Verification failed: %s\n", result.Data.ResponseCode) } ``` ```java Java VerifyResponse result = client.verifications().verify( "+13035551234", VerifyParams.builder() .code("17686") .verifyProfileId("4900017a-e7c8-e79e-0a7c-0d98f49b09cc") .build() ); if ("accepted".equals(result.getResponseCode())) { System.out.println("Verification successful!"); } else { System.out.println("Verification failed: " + result.getResponseCode()); } ``` ```csharp .NET var result = await service.VerifyAsync("+13035551234", new VerifyCodeOptions { Code = "17686", VerifyProfileId = "4900017a-e7c8-e79e-0a7c-0d98f49b09cc" }); if (result.Data.ResponseCode == "accepted") Console.WriteLine("Verification successful!"); else Console.WriteLine($"Verification failed: {result.Data.ResponseCode}"); ``` ```php PHP $result = \Telnyx\Verification::verify('+13035551234', [ 'code' => '17686', 'verify_profile_id' => '4900017a-e7c8-e79e-0a7c-0d98f49b09cc' ]); if ($result->response_code === 'accepted') { echo "Verification successful!\n"; } else { echo "Verification failed: " . $result->response_code . "\n"; } ``` ### Example response ```json { "data": { "phone_number": "+13035551234", "response_code": "accepted" } } ``` A `response_code` of `"accepted"` confirms the code matches. Other possible values: | Response Code | Meaning | |---------------|---------| | `accepted` | Code is correct - verification successful | | `rejected` | Code is incorrect | | `expired` | Verification timed out (exceeded `timeout_secs`) | | `max_attempts_exceeded` | Too many incorrect attempts | Telnyx Verify supports webhooks to receive instant notifications when users complete verification, eliminating the need for polling. This enables event-driven workflows for faster user experiences. Learn more: [Receiving Webhooks for Telnyx Verify](/docs/identity/verify/receiving-webhooks#real-time-verification-status-updates) ## Next steps Now that you've completed a basic verification flow, explore these guides to build a production-ready implementation: Create branded verification messages that match your app's voice and compliance requirements. Protect against SMS pumping, brute force attacks, and other verification fraud. Receive real-time notifications when verifications complete or expire. Complete login, registration, and payment verification flow implementations. --- ### DTMF confirmation > Source: https://developers.telnyx.com/docs/identity/verify/dtmf-confirm.md DTMF confirmation calls a phone number, plays a TTS prompt, and collects a single keypress (`1`) to confirm ownership. No verification code is generated — the keypress is the confirmation. The `POST /verifications/{id}/actions/verify` endpoint is **not used**. Verification completes on the call itself. DTMF confirmation is unique to Telnyx — Twilio and Vonage Verify APIs only support code-based voice verification (read a code, then type it). Single-keypress confirmation reduces user friction and works on landlines. ## Flow ```mermaid sequenceDiagram participant App participant Telnyx as Verify API participant Phone App->>Telnyx: POST /verifications/dtmf_confirm Telnyx->>Phone: Outbound call Phone-->>Telnyx: Answered Telnyx->>Phone: TTS prompt + DTMF gather Phone-->>Telnyx: Digit "1" Telnyx-->>App: Webhook: verification.complete (accepted) ``` | Outcome | Trigger | Status | |---------|---------|--------| | Confirmed | Digit `1` pressed | `accepted` | | Rejected | Wrong digit | `invalid` | | Timed out | No keypress (10s) | `expired` | | Failed | Call not answered | `error` | Up to 3 attempts per call. After 3 wrong digits, the call ends with status `invalid`. --- ## Use cases Confirm ownership before allowing a number as outbound Caller ID. Verify numbers that cannot receive SMS. Single keypress instead of reading and typing a code. Confirm phone ownership without code entry. --- ## Create a verify profile Create a profile with `dtmf_confirm` settings. This can be combined with other verification types (SMS, call) on the same profile. ```bash curl curl -X POST https://api.telnyx.com/v2/verify_profiles \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -d '{ "name": "caller-id-verification", "language": "en-US", "dtmf_confirm": { "default_timeout_secs": 300 } }' ``` ```python Python import os import telnyx telnyx.api_key = os.environ["TELNYX_API_KEY"] profile = telnyx.VerifyProfile.create( name="caller-id-verification", language="en-US", dtmf_confirm={"default_timeout_secs": 300}, ) profile_id = profile.id ``` ```javascript Node const telnyx = require('telnyx')(process.env.TELNYX_API_KEY); const profile = await telnyx.verifyProfiles.create({ name: 'caller-id-verification', language: 'en-US', dtmf_confirm: { default_timeout_secs: 300 }, }); const profileId = profile.data.id; ``` ```ruby Ruby require 'telnyx' Telnyx.api_key = ENV['TELNYX_API_KEY'] profile = Telnyx::VerifyProfile.create( name: 'caller-id-verification', language: 'en-US', dtmf_confirm: { default_timeout_secs: 300 } ) profile_id = profile.id ``` ```go Go package main import ( "os" telnyx "github.com/telnyx/telnyx-go" ) func main() { client := telnyx.NewClient(os.Getenv("TELNYX_API_KEY")) profile, _ := client.VerifyProfiles.Create(&telnyx.VerifyProfileParams{ Name: "caller-id-verification", Language: "en-US", DTMFConfirm: &telnyx.DTMFConfirmSettings{ DefaultTimeoutSecs: 300, }, }) profileID := profile.ID } ``` ```java Java import com.telnyx.sdk.*; import com.telnyx.sdk.api.VerifyApi; import com.telnyx.sdk.model.*; ApiClient client = Configuration.getDefaultApiClient(); client.setBearerToken(System.getenv("TELNYX_API_KEY")); VerifyApi api = new VerifyApi(client); CreateVerifyProfileRequest request = new CreateVerifyProfileRequest() .name("caller-id-verification") .language("en-US") .dtmfConfirm(new DTMFConfirmSettings().defaultTimeoutSecs(300)); VerifyProfileResponse profile = api.createVerifyProfile(request); String profileId = profile.getData().getId(); ``` ```csharp .NET using Telnyx; TelnyxConfiguration.SetApiKey(Environment.GetEnvironmentVariable("TELNYX_API_KEY")); var service = new VerifyProfileService(); var profile = service.Create(new VerifyProfileCreateOptions { Name = "caller-id-verification", Language = "en-US", DtmfConfirm = new DtmfConfirmSettings { DefaultTimeoutSecs = 300 } }); var profileId = profile.Id; ``` ```php PHP require 'vendor/autoload.php'; \Telnyx\Telnyx::setApiKey(getenv('TELNYX_API_KEY')); $profile = \Telnyx\VerifyProfile::create([ 'name' => 'caller-id-verification', 'language' => 'en-US', 'dtmf_confirm' => ['default_timeout_secs' => 300], ]); $profileId = $profile->id; ``` The returned `id` is required for verification requests. --- ## Trigger verification ```bash curl curl -X POST https://api.telnyx.com/v2/verifications/dtmf_confirm \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -d '{ "phone_number": "+13035551234", "verify_profile_id": "YOUR_PROFILE_ID" }' ``` ```python Python verification = telnyx.Verification.create( phone_number="+13035551234", verify_profile_id=profile_id, type="dtmf_confirm", ) ``` ```javascript Node const verification = await telnyx.verifications.create({ phone_number: '+13035551234', verify_profile_id: profileId, type: 'dtmf_confirm', }); ``` ```ruby Ruby verification = Telnyx::Verification.create( phone_number: '+13035551234', verify_profile_id: profile_id, type: 'dtmf_confirm' ) ``` ```go Go verification, _ := client.Verifications.Create(&telnyx.VerificationParams{ PhoneNumber: "+13035551234", VerifyProfileID: profileID, Type: "dtmf_confirm", }) ``` ```java Java CreateVerificationRequest verReq = new CreateVerificationRequest() .phoneNumber("+13035551234") .verifyProfileId(profileId) .type(CreateVerificationRequest.TypeEnum.DTMF_CONFIRM); VerificationResponse ver = api.createVerification(verReq); ``` ```csharp .NET var verService = new VerificationService(); var verification = verService.Create(new VerificationCreateOptions { PhoneNumber = "+13035551234", VerifyProfileId = profileId, Type = "dtmf_confirm" }); ``` ```php PHP $verification = \Telnyx\Verification::create([ 'phone_number' => '+13035551234', 'verify_profile_id' => $profileId, 'type' => 'dtmf_confirm', ]); ``` ### Response ```json { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "phone_number": "+13035551234", "record_type": "verification", "status": "pending", "type": "dtmf_confirm", "timeout_secs": 300, "verify_profile_id": "YOUR_PROFILE_ID", "created_at": "2026-02-20T15:30:00.000000", "updated_at": "2026-02-20T15:30:00.000000" } } ``` Default TTS prompt: > *"This is a verification call to confirm that this phone number is going to be used as a Caller ID for outbound calls. If you did not request this verification, or if someone is asking you to accept this call, please ignore this message. If you did request this verification, please press 1."* The TTS language is determined by the `language` field on the Verify Profile (default: `en-US`). --- ## Handle the result Verification completes on the call — no verify endpoint call needed. Receive the outcome via [webhooks](/docs/identity/verify/receiving-webhooks). ### Accepted (digit `1` pressed) ```json { "data": { "event_type": "verification.complete", "payload": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "phone_number": "+13035551234", "status": "accepted", "type": "dtmf_confirm", "verify_profile_id": "YOUR_PROFILE_ID" } } } ``` ### Failed (wrong digit, timeout, or call failure) ```json { "data": { "event_type": "verification.complete", "payload": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "phone_number": "+13035551234", "status": "invalid", "type": "dtmf_confirm", "verify_profile_id": "YOUR_PROFILE_ID" } } } ``` ### Polling alternative ```bash curl https://api.telnyx.com/v2/verifications/{verification_id} \ -H "Authorization: Bearer $TELNYX_API_KEY" ``` --- ## Complete example Full flow: create profile, trigger verification, handle webhook. ```python import os import telnyx from flask import Flask, request, jsonify telnyx.api_key = os.environ["TELNYX_API_KEY"] app = Flask(__name__) # Create profile (once) profile = telnyx.VerifyProfile.create( name="dtmf-verification", language="en-US", dtmf_confirm={"default_timeout_secs": 300}, ) # Trigger verification verification = telnyx.Verification.create( phone_number="+13035551234", verify_profile_id=profile.id, type="dtmf_confirm", ) print(f"Verification {verification.id} — {verification.status}") @app.route("/webhooks/verify", methods=["POST"]) def handle_webhook(): payload = request.json["data"]["payload"] if payload["status"] == "accepted": print(f"✅ {payload['phone_number']} verified") else: print(f"❌ {payload['phone_number']} failed: {payload['status']}") return jsonify({"status": "ok"}), 200 app.run(port=5000) ``` ```javascript const express = require('express'); const telnyx = require('telnyx')(process.env.TELNYX_API_KEY); const app = express(); app.use(express.json()); (async () => { // Create profile (once) const profile = await telnyx.verifyProfiles.create({ name: 'dtmf-verification', language: 'en-US', dtmf_confirm: { default_timeout_secs: 300 }, }); // Trigger verification const verification = await telnyx.verifications.create({ phone_number: '+13035551234', verify_profile_id: profile.data.id, type: 'dtmf_confirm', }); console.log(`Verification ${verification.data.id} — ${verification.data.status}`); // Handle webhook app.post('/webhooks/verify', (req, res) => { const { phone_number, status } = req.body.data.payload; if (status === 'accepted') { console.log(`✅ ${phone_number} verified`); } else { console.log(`❌ ${phone_number} failed: ${status}`); } res.json({ status: 'ok' }); }); app.listen(5000, () => console.log('Webhook server on :5000')); })(); ``` ```ruby require 'telnyx' require 'sinatra' require 'json' Telnyx.api_key = ENV['TELNYX_API_KEY'] # Create profile (once) profile = Telnyx::VerifyProfile.create( name: 'dtmf-verification', language: 'en-US', dtmf_confirm: { default_timeout_secs: 300 } ) # Trigger verification verification = Telnyx::Verification.create( phone_number: '+13035551234', verify_profile_id: profile.id, type: 'dtmf_confirm' ) puts "Verification #{verification.id} — #{verification.status}" # Handle webhook post '/webhooks/verify' do payload = JSON.parse(request.body.read)['data']['payload'] if payload['status'] == 'accepted' puts "✅ #{payload['phone_number']} verified" else puts "❌ #{payload['phone_number']} failed: #{payload['status']}" end { status: 'ok' }.to_json end ``` ```go package main import ( "encoding/json" "fmt" "net/http" "os" telnyx "github.com/telnyx/telnyx-go" ) func main() { client := telnyx.NewClient(os.Getenv("TELNYX_API_KEY")) // Create profile (once) profile, _ := client.VerifyProfiles.Create(&telnyx.VerifyProfileParams{ Name: "dtmf-verification", Language: "en-US", DTMFConfirm: &telnyx.DTMFConfirmSettings{ DefaultTimeoutSecs: 300, }, }) // Trigger verification ver, _ := client.Verifications.Create(&telnyx.VerificationParams{ PhoneNumber: "+13035551234", VerifyProfileID: profile.ID, Type: "dtmf_confirm", }) fmt.Printf("Verification %s — %s\n", ver.ID, ver.Status) // Handle webhook http.HandleFunc("/webhooks/verify", func(w http.ResponseWriter, r *http.Request) { var event struct { Data struct { Payload struct { PhoneNumber string `json:"phone_number"` Status string `json:"status"` } `json:"payload"` } `json:"data"` } json.NewDecoder(r.Body).Decode(&event) p := event.Data.Payload if p.Status == "accepted" { fmt.Printf("✅ %s verified\n", p.PhoneNumber) } else { fmt.Printf("❌ %s failed: %s\n", p.PhoneNumber, p.Status) } w.Write([]byte(`{"status":"ok"}`)) }) http.ListenAndServe(":5000", nil) } ``` --- ## Verification type comparison | Feature | SMS | Call | Flash Call | DTMF Confirm | |---------|-----|------|-----------|-------------| | **User action** | Type code | Listen + type code | None | Press 1 | | **Code generated** | Yes | Yes | Yes (caller ID) | No | | **Verify endpoint** | Required | Required | Required | Not needed | | **Landline support** | No | Yes | No | Yes | | **Fraud risk** | SIM swap, interception | Low | Low | Low | | **Competitor support** | Twilio, Vonage | Twilio, Vonage | Twilio | Telnyx only | --- ## Troubleshooting Verification times out with status `expired`. Implement a retry with delay, or fall back to SMS. Up to 3 attempts per call. After 3 failures, status is `invalid`. Trigger a new verification to retry. Call was not answered and no webhook received. Verify the webhook URL is configured and reachable. Poll the status endpoint as fallback. The prompt is fixed to the standard verification message. Voice and language are determined by the Verify Profile's `language` setting. Custom prompt text is not yet supported. Standard Verify API rate limits apply. Avoid triggering multiple concurrent verifications for the same phone number — the previous call must complete or time out first. --- ## Next steps Real-time verification status updates. Branded verification messages for SMS and call types. Full API specification. SMS and call verification guide. --- ### Custom templates > Source: https://developers.telnyx.com/docs/identity/verify/custom-templates.md Telnyx Verify now enables you to create and register your own verification message templates instantly. This feature allows you to build compliant, branded one-time password (OTP) messages that match your brand voice while maintaining security standards. ## Overview Custom templates give you complete control over your verification messaging. Instead of using pre-built templates, you can create personalized messages that: - Align with your brand guidelines and tone of voice. - Include industry-specific security warnings and disclaimers. - Maintain compliance with regulatory requirements. - Deploy instantly without manual approval processes. ## When to use custom templates **Use custom templates when:** - You need branded verification messages that match your company's voice. - Your industry requires specific compliance language in OTP messages. - You want to include custom security warnings or instructions. - You need different templates for different user segments or regions. **Use pre-built templates when:** - You're getting started quickly and don't need customization. - Standard verification messages meet your requirements. - You want to minimize setup time. ## Template variables All verification templates support the following variables that will be automatically replaced when sending verifications: - `{{app_name}}` - Your application name as configured in the verify profile. - `{{code}}` - The verification code sent to the user. ## Creating custom templates Use the Telnyx API to create custom verification message templates. ### Create a custom template ```bash curl -L 'https://api.telnyx.com/v2/verify_profiles/templates' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -d '{ "text": "Your {{app_name}} verification code is {{code}}. Do not share this code." }' ``` ### Example response ```json { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "text": "Your {{app_name}} verification code is {{code}}. Do not share this code." } } ``` Save the template `id` returned in the response. You'll need this ID to reference the template when creating verify profiles. For complete API details, see the [Create Message Template API reference](/api-reference/verify/create-message-template). ## Real-world template examples Here are industry-specific template examples to inspire your custom messaging: ### E-commerce and retail ``` Your {{app_name}} verification code is {{code}}. Complete your purchase securely. ``` ### Banking and finance ``` {{code}} is your {{app_name}} security code. Never share this with anyone, including our staff. ``` ### Healthcare and telemedicine ``` Your {{app_name}} appointment verification code: {{code}}. If you didn't request this, contact support immediately. ``` ### Social media and gaming ``` Welcome to {{app_name}}! Your verification code is {{code}}. Start connecting with friends now. ``` ### Delivery and logistics ``` Your {{app_name}} delivery confirmation code is {{code}}. Track your package now. ``` ### Travel and hospitality ``` {{code}} is your {{app_name}} booking verification code. ``` ## Using custom templates in verify profiles Once you've created a custom template, reference its ID when creating or updating a verify profile. ### Example: Create verify profile with custom template ```bash curl -L 'https://api.telnyx.com/v2/verify_profiles' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -d '{ "name": "my-app-verification", "language": "en-US", "sms": { "messaging_template_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "whitelisted_destinations": ["US", "CA"], "default_timeout_secs": 300, "code_length": 6 } }' ``` The `messaging_template_id` field references your custom template. When you send verifications using this profile, your custom template will be used instead of the default. ## Updating custom templates You can update existing custom templates to refine messaging or fix errors. ### Update a template ```bash curl -L -X PATCH 'https://api.telnyx.com/v2/verify_profiles/templates/a1b2c3d4-e5f6-7890-abcd-ef1234567890' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -d '{ "text": "Your {{app_name}} verification code is {{code}}. Contact support if you did not request this." }' ``` ### Example response ```json { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "text": "Your {{app_name}} verification code is {{code}}. Contact support if you did not request this." } } ``` Updating a template affects all verify profiles that reference it. Test template changes carefully before updating production templates. For complete API details, see the [Update Message Template API reference](/api-reference/verify/update-message-template). ## Best practices ### Keep messages concise SMS messages have character limits. Keep your templates under 160 characters when possible to avoid message splitting and additional costs. ### Include security warnings For sensitive use cases like banking or healthcare, include warnings about not sharing codes: ``` Never share this code with anyone, including our staff. If you didn't request this, contact support immediately. ``` ### Test before production Always test new templates in a development environment before using them in production verify profiles. Send test verifications to confirm: - Variables are replaced correctly. - Message formatting appears as expected. - Character count is within limits. - Links or special characters display properly. ### Follow compliance guidelines Ensure your templates comply with: - Industry regulations (HIPAA, PCI-DSS, GDPR, etc.). - Carrier requirements for SMS messaging. - Local laws regarding verification messages. - Your organization's security policies. ### Consider localization If you serve multiple regions, create separate templates for different languages and cultures. Use the verify profile's `language` parameter to organize templates by locale. ## Next steps - Learn how to send verifications: [Telnyx Verify quickstart](/docs/identity/verify/quickstart) - Configure webhooks for verification events: [Receiving webhooks](/docs/identity/verify/receiving-webhooks) - View the complete Verify API reference: [Verify API documentation](/api-reference/verify/create-a-verify-profile) --- ### Webhooks > Source: https://developers.telnyx.com/docs/identity/verify/receiving-webhooks.md You can choose to be notified about events on your send verifications by configuring webhooks on your verify profile. *** ## Verify delivery status updates The Telnyx Verify Service will attempt to notify you about the following delivery events for the send verification request: * verify.sent * verify.failed * verify.delivered #### Delivery status Payload Here is an example of a webhook event where a delivery receipt is returned to the sender after sending a verification: ```json { "data": { "event_type": "verify.delivered", "id": "3bb6321b-abd5-4d60-8cf8-1e6026bb1c41", "occurred_at": "2025-10-08T17:16:09.602+00:00", "payload": { "created_at": "2025-10-08T17:16:08.436941", "custom_code": "25565", "delivery_status": "delivered", "extension": null, "failed_attempts": 0, "id": "010d9dee-d86d-47d0-8d6c-20c0f95a79ec", "mccmnc": null, "phone_number": "+13125000000", "profile_id": "49000192-1bdb-c56f-3de7-e008ef6da16b", "record_type": "verification", "status": "pending", "timeout_secs": null, "type": "sms", "updated_at": "2025-10-08T17:16:09.592765" }, "record_type": "event" }, "meta": { "attempt": 1, "delivered_to": "https://webhook.site/af3a92e7-e150-442c-9fe6-61658ce26b1a" } } ``` #### Delivery statuses Delivery Status Description sent The verification has been sent to the upstream provider. delivered The upstream provider has confirmed delivery of the verification. sending_failed Telnyx has failed to send the verification to the upstream provider. Please reach out to our support if you have received this status. delivery_failed The upstream provider has failed to send the verification to the receiver. Please reach out to our support if you have received this status. delivery_unconfirmed There is no indication whether or not the verification has reached the receiver. Please reach out to our support if you have received this status ## Real-time verification status updates Telnyx Verify now supports webhooks for instant verification status notifications, eliminating the need to poll the API for verification completion. ### Benefits * **Eliminates polling** - No need to repeatedly check verification status through API calls. * **Event-driven workflow** - React immediately to verification state changes in real-time. * **Faster user experience** - Process verification completions instantly without delays. * **Improved efficiency** - Reduces server load and unnecessary API requests. Configure webhooks on your Verify profile to receive real-time updates when verifications complete. For specific webhook event types, payload structures, and configuration details, refer to the [Telnyx API documentation](https://developers.telnyx.com/api-reference/profiles/list-messaging-profiles#list-messaging-profiles) or contact Telnyx support. ### Telnyx Webhook IPs If you use an ACL or Firewall on your network, make sure you whitelist the following subnet(s): 192.76.120.192/27 --- ### Security Best Practices > Source: https://developers.telnyx.com/docs/identity/verify/security-best-practices.md Verification flows are a high-value target for attackers. SMS pumping, toll fraud, brute-force code guessing, and social engineering can cost you money and compromise user accounts. This guide covers practical defenses you should implement alongside Telnyx Verify. ## Threat overview | Threat | Description | Impact | |--------|-------------|--------| | **SMS pumping** | Attackers trigger thousands of SMS verifications to premium-rate numbers | Inflated costs, sometimes $10K+ per incident | | **Toll fraud (IRSF)** | Exploiting call verification to generate revenue on premium international numbers | Per-minute charges on fraudulent calls | | **Brute-force attacks** | Systematically guessing verification codes | Account takeover | | **Code interception** | SIM swapping, SS7 attacks, malware intercepting SMS | Account compromise | | **Enumeration** | Using verification endpoints to check if phone numbers exist in your system | Privacy leak, targeted attacks | ## Rate limiting Rate limiting is your first line of defense against abuse. Apply limits at multiple layers. ### Per-phone-number limits Restrict how many verification attempts a single phone number can trigger within a time window. ```javascript Node import { RateLimiterMemory } from 'rate-limiter-flexible'; // Max 3 verification requests per phone number per 10 minutes const phoneLimiter = new RateLimiterMemory({ points: 3, duration: 600, // 10 minutes }); async function requestVerification(phoneNumber) { try { await phoneLimiter.consume(phoneNumber); // Proceed with Telnyx Verify API call } catch (rejRes) { const retryAfter = Math.ceil(rejRes.msBeforeNext / 1000); throw new Error(`Too many attempts. Try again in ${retryAfter} seconds.`); } } ``` ```python Python from datetime import datetime, timedelta # Simple in-memory rate limiter (use Redis in production) verification_attempts = {} def check_rate_limit(phone_number, max_attempts=3, window_minutes=10): now = datetime.utcnow() cutoff = now - timedelta(minutes=window_minutes) # Clean old attempts attempts = verification_attempts.get(phone_number, []) attempts = [t for t in attempts if t > cutoff] if len(attempts) >= max_attempts: raise Exception(f"Too many attempts. Try again later.") attempts.append(now) verification_attempts[phone_number] = attempts return True ``` ```go Go package main import ( "fmt" "sync" "time" ) type RateLimiter struct { mu sync.Mutex attempts map[string][]time.Time max int window time.Duration } func NewRateLimiter(max int, window time.Duration) *RateLimiter { return &RateLimiter{ attempts: make(map[string][]time.Time), max: max, window: window, } } func (rl *RateLimiter) Allow(key string) error { rl.mu.Lock() defer rl.mu.Unlock() cutoff := time.Now().Add(-rl.window) var valid []time.Time for _, t := range rl.attempts[key] { if t.After(cutoff) { valid = append(valid, t) } } if len(valid) >= rl.max { return fmt.Errorf("too many attempts, try again later") } rl.attempts[key] = append(valid, time.Now()) return nil } ``` ### Per-IP address limits Prevent a single IP from triggering verifications for many different numbers (a hallmark of SMS pumping): ```javascript Node // Max 10 verification requests per IP per hour const ipLimiter = new RateLimiterMemory({ points: 10, duration: 3600, }); app.post('/verify/request', async (req, res) => { const clientIp = req.ip; try { await ipLimiter.consume(clientIp); await phoneLimiter.consume(req.body.phone_number); // Proceed with verification } catch { res.status(429).json({ error: 'Too many requests' }); } }); ``` ```python Python from flask import Flask, request, jsonify from flask_limiter import Limiter from flask_limiter.util import get_remote_address app = Flask(__name__) limiter = Limiter(app=app, key_func=get_remote_address) @app.route("/verify/request", methods=["POST"]) @limiter.limit("10 per hour") def request_verification(): phone_number = request.json["phone_number"] check_rate_limit(phone_number) # Proceed with Telnyx Verify API call return jsonify(success=True) ``` ### Recommended limits | Scope | Limit | Window | |-------|-------|--------| | Per phone number | 3 attempts | 10 minutes | | Per phone number | 5 attempts | 1 hour | | Per IP address | 10 attempts | 1 hour | | Per account/session | 5 attempts | 1 hour | | Global (all numbers) | Monitor for spikes | Continuous | ## SMS pumping prevention SMS pumping is the most costly fraud vector for verification flows. Attackers abuse your send endpoint to generate SMS revenue on number ranges they control. ### Detection signals Watch for these patterns: - **Sequential numbers** — Verification requests for `+1234500001`, `+1234500002`, `+1234500003`... - **Unusual country codes** — Spike in verifications to countries you don't serve - **High failure rate** — Many verifications triggered but never completed - **Burst traffic** — Sudden spike in verification requests from a single source ### Defenses Configure `whitelisted_destinations` on your Verify profile to only allow countries where your users are: ```bash curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sms": { "whitelisted_destinations": ["US", "CA", "GB"] } }' ``` Don't expose your verification endpoint to unauthenticated users. Require at least a session or account to trigger a verification. Place a CAPTCHA (reCAPTCHA, hCaptcha, Turnstile) before the phone number input to block automated submissions. Set up alerts for unusual verification volume: ```javascript // Track verification requests per minute const verifyCount = new Map(); function trackVerification() { const minute = Math.floor(Date.now() / 60000); verifyCount.set(minute, (verifyCount.get(minute) || 0) + 1); if (verifyCount.get(minute) > 100) { // Alert: possible SMS pumping attack alertOps('Verification spike detected: ' + verifyCount.get(minute) + '/min'); } } ``` ## Code security ### Use appropriate code length Longer codes are harder to brute-force but harder for users to enter. Balance security and usability: | Code Length | Combinations | Brute-force time (3 attempts/min) | Recommendation | |-------------|-------------|-----------------------------------|----------------| | 4 digits | 10,000 | ~55 hours | Low security only | | 5 digits | 100,000 | ~23 days | Default — good balance | | 6 digits | 1,000,000 | ~231 days | High security applications | Configure code length in your Verify profile: ```bash curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sms": { "code_length": 6 } }' ``` ### Set appropriate timeouts Short timeouts reduce the window for brute-force attacks: ```bash curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sms": { "default_timeout_secs": 300 }, "call": { "default_timeout_secs": 300 } }' ``` A 5-minute timeout (300 seconds) works well for most applications. Shorter timeouts (120s) add security but may frustrate users on slow networks. ### Limit verification attempts Lock out after too many failed code entries to prevent brute-force: ```javascript Node const failedAttempts = new Map(); async function verifyCode(phoneNumber, code) { const attempts = failedAttempts.get(phoneNumber) || 0; if (attempts >= 5) { throw new Error('Too many failed attempts. Request a new code.'); } const response = await fetch( `https://api.telnyx.com/v2/verifications/by_phone_number/${phoneNumber}/actions/verify`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.TELNYX_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ code, verify_profile_id: PROFILE_ID }), } ); const result = await response.json(); if (result.data.response_code === 'accepted') { failedAttempts.delete(phoneNumber); return true; } failedAttempts.set(phoneNumber, attempts + 1); return false; } ``` ```python Python import requests import os failed_attempts = {} # Use Redis in production def verify_code(phone_number, code, profile_id): attempts = failed_attempts.get(phone_number, 0) if attempts >= 5: raise Exception("Too many failed attempts. Request a new code.") response = requests.post( f"https://api.telnyx.com/v2/verifications/by_phone_number/{phone_number}/actions/verify", headers={"Authorization": f"Bearer {os.environ['TELNYX_API_KEY']}"}, json={"code": code, "verify_profile_id": profile_id}, ) result = response.json() if result["data"]["response_code"] == "accepted": failed_attempts.pop(phone_number, None) return True failed_attempts[phone_number] = attempts + 1 return False ``` ## Prevent number enumeration Don't reveal whether a phone number exists in your system through verification responses: **❌ Vulnerable** — reveals whether the number is registered: ```json { "error": "No account found for this phone number" } ``` **✅ Secure** — same response regardless: ```json { "message": "If this number is registered, you'll receive a verification code." } ``` Always return a consistent response and send the verification (or silently drop it) regardless of whether the number exists in your system. ## Channel fallback strategy Use multiple verification channels to improve delivery and security: Start with SMS verification — widest reach and fastest delivery. If SMS isn't delivered within 30 seconds, offer a voice call option. This helps users on networks with delayed SMS delivery. For supported markets, flashcall verification (where the phone number itself is the code) provides instant verification with no user input required. Configure all three channels on your Verify profile: ```bash curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sms": { "whitelisted_destinations": ["US", "CA"], "default_timeout_secs": 300, "code_length": 6 }, "call": { "default_timeout_secs": 300 }, "flashcall": { "default_timeout_secs": 300 } }' ``` ## Webhook security for Verify Secure your verification webhook endpoint to prevent spoofed delivery notifications: 1. **Allowlist Telnyx IPs** — Only accept webhooks from `192.76.120.192/27` 2. **Use HTTPS** — Never use plain HTTP for webhook endpoints 3. **Validate payload structure** — Check for expected fields before processing 4. **Don't trust client-side status** — Always verify through webhooks or API, never trust client-reported verification status ```javascript Node import { createServer } from 'http'; import { networkInterfaces } from 'os'; const TELNYX_WEBHOOK_CIDR = '192.76.120.192/27'; function isFromTelnyx(ip) { // In production, use a proper CIDR matching library const parts = ip.split('.').map(Number); return parts[0] === 192 && parts[1] === 76 && parts[2] === 120 && parts[3] >= 192 && parts[3] <= 223; } app.post('/webhooks/verify', (req, res) => { const clientIp = req.headers['x-forwarded-for']?.split(',')[0] || req.ip; if (!isFromTelnyx(clientIp)) { console.warn(`Rejected webhook from unauthorized IP: ${clientIp}`); return res.sendStatus(403); } // Process webhook const event = req.body.data; if (event.event_type === 'verify.delivered') { console.log(`Verification delivered to ${event.payload.phone_number}`); } res.sendStatus(200); }); ``` ```python Python import ipaddress from flask import Flask, request, jsonify TELNYX_WEBHOOK_SUBNET = ipaddress.ip_network("192.76.120.192/27") @app.route("/webhooks/verify", methods=["POST"]) def handle_verify_webhook(): client_ip = ipaddress.ip_address( request.headers.get("X-Forwarded-For", request.remote_addr).split(",")[0].strip() ) if client_ip not in TELNYX_WEBHOOK_SUBNET: return jsonify(error="Forbidden"), 403 event = request.json["data"] if event["event_type"] == "verify.delivered": print(f"Verification delivered to {event['payload']['phone_number']}") return jsonify(success=True), 200 ``` ## Security checklist Use this checklist when implementing Telnyx Verify in production: - [ ] Per-phone-number rate limit (3/10min) - [ ] Per-IP rate limit (10/hour) - [ ] Per-account/session rate limit - [ ] Global volume monitoring and alerting - [ ] Country allowlist configured on Verify profile - [ ] CAPTCHA before verification trigger - [ ] Authentication required before sending verification - [ ] SMS pumping detection (sequential numbers, country spikes) - [ ] Appropriate code length (5-6 digits) - [ ] Short timeout (300 seconds or less) - [ ] Max failed attempts lockout (5 attempts) - [ ] Consistent responses (no number enumeration) - [ ] HTTPS webhook endpoints - [ ] Telnyx IP allowlisting for webhooks - [ ] Server-side verification only (never trust client) - [ ] Logging and monitoring for anomalies ## Next steps Set up your first verification flow Brand your verification messages Receive real-time verification status updates Advanced fraud prevention strategies --- ### Rate Limiting & Fraud Prevention > Source: https://developers.telnyx.com/docs/identity/verify/rate-limiting-fraud-prevention.md This guide goes deep on the operational side of protecting your Telnyx Verify integration — server-side rate limiting architectures, geo-fencing, anomaly detection, cost controls, and incident response. For foundational security concepts, see the [Security Best Practices](/docs/identity/verify/security-best-practices) guide. ## Architecture overview A robust fraud prevention system layers multiple defenses: ``` User Request → CAPTCHA → IP Rate Limit → Phone Rate Limit → Geo-fence → Anomaly Check → Telnyx Verify API ``` Each layer catches different attack patterns. No single defense is sufficient on its own. ## Server-side rate limiting with Redis Production rate limiting requires a distributed store. These examples use Redis for shared state across multiple application instances. ### Sliding window rate limiter ```javascript Node import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL); /** * Sliding window rate limiter using Redis sorted sets. * @param {string} key - Rate limit key (e.g., phone number or IP) * @param {number} maxRequests - Maximum requests allowed * @param {number} windowMs - Time window in milliseconds * @returns {Promise<{allowed: boolean, remaining: number, retryAfterMs: number}>} */ async function checkRateLimit(key, maxRequests, windowMs) { const now = Date.now(); const windowStart = now - windowMs; const redisKey = `ratelimit:${key}`; const pipeline = redis.pipeline(); pipeline.zremrangebyscore(redisKey, 0, windowStart); // Remove expired pipeline.zcard(redisKey); // Count current pipeline.zadd(redisKey, now, `${now}-${Math.random()}`); // Add this request pipeline.expire(redisKey, Math.ceil(windowMs / 1000)); // Set TTL const results = await pipeline.exec(); const currentCount = results[1][1]; if (currentCount >= maxRequests) { // Remove the entry we just added await redis.zremrangebyscore(redisKey, now, now); const oldestEntry = await redis.zrange(redisKey, 0, 0, 'WITHSCORES'); const retryAfterMs = oldestEntry.length > 1 ? windowMs - (now - Number(oldestEntry[1])) : windowMs; return { allowed: false, remaining: 0, retryAfterMs }; } return { allowed: true, remaining: maxRequests - currentCount - 1, retryAfterMs: 0 }; } // Usage: Multi-layer rate limiting async function handleVerificationRequest(req) { const phone = req.body.phone_number; const ip = req.ip; const userId = req.user?.id; // Layer 1: IP rate limit (10/hour) const ipCheck = await checkRateLimit(`ip:${ip}`, 10, 3600000); if (!ipCheck.allowed) { return { status: 429, retryAfter: ipCheck.retryAfterMs }; } // Layer 2: Phone rate limit (3/10min) const phoneCheck = await checkRateLimit(`phone:${phone}`, 3, 600000); if (!phoneCheck.allowed) { return { status: 429, retryAfter: phoneCheck.retryAfterMs }; } // Layer 3: User rate limit (5/hour) if (userId) { const userCheck = await checkRateLimit(`user:${userId}`, 5, 3600000); if (!userCheck.allowed) { return { status: 429, retryAfter: userCheck.retryAfterMs }; } } // All checks passed — send verification return await sendVerification(phone); } ``` ```python Python import time import math import redis import os import random r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379")) def check_rate_limit(key: str, max_requests: int, window_seconds: int) -> dict: """Sliding window rate limiter using Redis sorted sets.""" now = time.time() window_start = now - window_seconds redis_key = f"ratelimit:{key}" pipe = r.pipeline() pipe.zremrangebyscore(redis_key, 0, window_start) pipe.zcard(redis_key) pipe.zadd(redis_key, {f"{now}-{random.random()}": now}) pipe.expire(redis_key, window_seconds + 1) results = pipe.execute() current_count = results[1] if current_count >= max_requests: r.zremrangebyscore(redis_key, now, now) oldest = r.zrange(redis_key, 0, 0, withscores=True) retry_after = window_seconds - (now - oldest[0][1]) if oldest else window_seconds return {"allowed": False, "remaining": 0, "retry_after_seconds": math.ceil(retry_after)} return {"allowed": True, "remaining": max_requests - current_count - 1, "retry_after_seconds": 0} def handle_verification_request(phone_number: str, ip: str, user_id: str = None): # Layer 1: IP rate limit (10/hour) ip_check = check_rate_limit(f"ip:{ip}", 10, 3600) if not ip_check["allowed"]: return {"error": "Too many requests", "retry_after": ip_check["retry_after_seconds"]} # Layer 2: Phone rate limit (3/10min) phone_check = check_rate_limit(f"phone:{phone_number}", 3, 600) if not phone_check["allowed"]: return {"error": "Too many attempts for this number", "retry_after": phone_check["retry_after_seconds"]} # Layer 3: User rate limit (5/hour) if user_id: user_check = check_rate_limit(f"user:{user_id}", 5, 3600) if not user_check["allowed"]: return {"error": "Too many requests", "retry_after": user_check["retry_after_seconds"]} return send_verification(phone_number) ``` ```ruby Ruby require "redis" redis = Redis.new(url: ENV["REDIS_URL"] || "redis://localhost:6379") def check_rate_limit(redis, key, max_requests, window_seconds) now = Time.now.to_f window_start = now - window_seconds redis_key = "ratelimit:#{key}" redis.pipelined do |pipe| pipe.zremrangebyscore(redis_key, 0, window_start) pipe.zcard(redis_key) pipe.zadd(redis_key, now, "#{now}-#{rand}") pipe.expire(redis_key, window_seconds + 1) end => [_, current_count, _, _] if current_count >= max_requests redis.zremrangebyscore(redis_key, now, now) oldest = redis.zrange(redis_key, 0, 0, with_scores: true) retry_after = oldest.any? ? window_seconds - (now - oldest[0][1]) : window_seconds { allowed: false, remaining: 0, retry_after: retry_after.ceil } else { allowed: true, remaining: max_requests - current_count - 1, retry_after: 0 } end end ``` ```go Go package ratelimit import ( "context" "fmt" "math/rand" "time" "github.com/redis/go-redis/v9" ) type Result struct { Allowed bool Remaining int RetryAfterMs int64 } func CheckRateLimit(ctx context.Context, rdb *redis.Client, key string, maxReqs int, window time.Duration) (*Result, error) { now := time.Now() windowStart := now.Add(-window) redisKey := fmt.Sprintf("ratelimit:%s", key) pipe := rdb.Pipeline() pipe.ZRemRangeByScore(ctx, redisKey, "0", fmt.Sprintf("%f", float64(windowStart.UnixMilli()))) countCmd := pipe.ZCard(ctx, redisKey) member := fmt.Sprintf("%d-%f", now.UnixMilli(), rand.Float64()) pipe.ZAdd(ctx, redisKey, redis.Z{Score: float64(now.UnixMilli()), Member: member}) pipe.Expire(ctx, redisKey, window+time.Second) _, err := pipe.Exec(ctx) if err != nil { return nil, err } count := countCmd.Val() if count >= int64(maxReqs) { rdb.ZRem(ctx, redisKey, member) return &Result{Allowed: false, Remaining: 0, RetryAfterMs: window.Milliseconds()}, nil } return &Result{Allowed: true, Remaining: maxReqs - int(count) - 1, RetryAfterMs: 0}, nil } ``` ## Geo-fencing Restrict verifications to countries where your service operates. This is the single most effective defense against SMS pumping. ### Configure on Verify profile ```bash curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sms": { "whitelisted_destinations": ["US", "CA", "GB", "AU"] } }' ``` ### Application-level geo-validation Add server-side validation before calling the API as a defense-in-depth measure: ```javascript Node import { parsePhoneNumber } from 'libphonenumber-js'; const ALLOWED_COUNTRIES = new Set(['US', 'CA', 'GB', 'AU']); function validatePhoneCountry(phoneNumber) { const parsed = parsePhoneNumber(phoneNumber); if (!parsed || !parsed.country) { throw new Error('Invalid phone number'); } if (!ALLOWED_COUNTRIES.has(parsed.country)) { throw new Error('Verification not available in this region'); } return parsed.country; } ``` ```python Python import phonenumbers ALLOWED_COUNTRIES = {"US", "CA", "GB", "AU"} def validate_phone_country(phone_number: str) -> str: parsed = phonenumbers.parse(phone_number) country = phonenumbers.region_code_for_number(parsed) if country not in ALLOWED_COUNTRIES: raise ValueError("Verification not available in this region") return country ``` ### High-risk country codes These country codes are frequently targeted for SMS pumping and toll fraud. Block or add extra scrutiny: | Code | Country | Risk | |------|---------|------| | +232 | Sierra Leone | SMS pumping | | +225 | Côte d'Ivoire | SMS pumping | | +233 | Ghana | SMS pumping | | +234 | Nigeria | Mixed (legitimate + fraud) | | +260 | Zambia | SMS pumping | | +256 | Uganda | SMS pumping | | +880 | Bangladesh | Toll fraud | | +855 | Cambodia | Toll fraud | | +856 | Laos | Toll fraud | | +960 | Maldives | Toll fraud | | +592 | Guyana | Toll fraud | **Note:** These are statistical patterns, not blanket rules. If you serve users in these countries, implement stronger rate limiting rather than blocking. ## Anomaly detection Build automated detection for suspicious patterns beyond simple rate limits. ### Conversion rate monitoring A healthy verification flow has a 60-80% conversion rate (codes sent vs. codes verified). A rate below 20% may indicate an attack. ```javascript Node class ConversionMonitor { constructor(redis, alertCallback) { this.redis = redis; this.alertCallback = alertCallback; } async trackSent(phoneNumber) { const hour = Math.floor(Date.now() / 3600000); await this.redis.incr(`verify:sent:${hour}`); await this.redis.expire(`verify:sent:${hour}`, 7200); } async trackVerified(phoneNumber) { const hour = Math.floor(Date.now() / 3600000); await this.redis.incr(`verify:verified:${hour}`); await this.redis.expire(`verify:verified:${hour}`, 7200); } async checkConversionRate() { const hour = Math.floor(Date.now() / 3600000); const sent = parseInt(await this.redis.get(`verify:sent:${hour}`)) || 0; const verified = parseInt(await this.redis.get(`verify:verified:${hour}`)) || 0; if (sent < 10) return; // Too few samples const rate = verified / sent; if (rate < 0.2) { this.alertCallback({ message: `Low verification conversion rate: ${(rate * 100).toFixed(1)}%`, sent, verified, hour: new Date(hour * 3600000).toISOString(), }); } } } ``` ```python Python import time import redis as redis_lib class ConversionMonitor: def __init__(self, redis_client, alert_callback): self.redis = redis_client self.alert = alert_callback def _hour_key(self): return int(time.time() // 3600) def track_sent(self): hour = self._hour_key() self.redis.incr(f"verify:sent:{hour}") self.redis.expire(f"verify:sent:{hour}", 7200) def track_verified(self): hour = self._hour_key() self.redis.incr(f"verify:verified:{hour}") self.redis.expire(f"verify:verified:{hour}", 7200) def check_conversion_rate(self): hour = self._hour_key() sent = int(self.redis.get(f"verify:sent:{hour}") or 0) verified = int(self.redis.get(f"verify:verified:{hour}") or 0) if sent < 10: return rate = verified / sent if rate < 0.2: self.alert({ "message": f"Low verification conversion rate: {rate:.1%}", "sent": sent, "verified": verified, }) ``` ### Sequential number detection SMS pumping often uses sequential phone numbers. Detect and block this pattern: ```javascript function detectSequentialNumbers(recentNumbers, threshold = 5) { if (recentNumbers.length < threshold) return false; // Sort by numeric value const sorted = recentNumbers .map(n => BigInt(n.replace(/\D/g, ''))) .sort((a, b) => (a < b ? -1 : 1)); // Check for sequences let sequential = 1; for (let i = 1; i < sorted.length; i++) { if (sorted[i] - sorted[i - 1] <= 3n) { sequential++; if (sequential >= threshold) return true; } else { sequential = 1; } } return false; } ``` ## Cost controls ### Set spend alerts Monitor your Telnyx account spending and set alerts at the account level through the [Telnyx Portal billing settings](https://portal.telnyx.com/#/app/billing). ### Implement circuit breakers Automatically disable verifications when anomalies are detected: ```javascript class VerificationCircuitBreaker { constructor(redis, maxPerHour = 500) { this.redis = redis; this.maxPerHour = maxPerHour; this.tripped = false; } async canSend() { if (this.tripped) return false; const hour = Math.floor(Date.now() / 3600000); const count = parseInt(await this.redis.get(`verify:total:${hour}`)) || 0; if (count >= this.maxPerHour) { this.tripped = true; // Alert operations team console.error(`Circuit breaker tripped: ${count} verifications in current hour`); return false; } await this.redis.incr(`verify:total:${hour}`); await this.redis.expire(`verify:total:${hour}`, 7200); return true; } reset() { this.tripped = false; } } ``` ## Incident response When you detect a fraud attack in progress: Stop all verification sends to limit financial damage. Look at the destination countries, IP addresses, and phone number patterns in your logs. Remove affected countries from your Verify profile's `whitelisted_destinations`. Reduce rate limits, add CAPTCHA if not present, and re-enable verifications gradually. Report the incident to [Telnyx Support](https://support.telnyx.com) for investigation and potential charge reversal. ## Configuration reference Summary of all Verify profile settings relevant to fraud prevention: | Setting | Endpoint | Purpose | |---------|----------|---------| | `whitelisted_destinations` | `PATCH /v2/verify_profiles/{id}` | Restrict SMS to specific countries | | `code_length` | `PATCH /v2/verify_profiles/{id}` | Set verification code length (4-10) | | `default_timeout_secs` | `PATCH /v2/verify_profiles/{id}` | Expiration time for codes | ```bash # Example: Production-hardened Verify profile curl -X PATCH "https://api.telnyx.com/v2/verify_profiles/YOUR_PROFILE_ID" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sms": { "whitelisted_destinations": ["US", "CA"], "default_timeout_secs": 300, "code_length": 6 }, "call": { "default_timeout_secs": 300 } }' ``` ## Next steps Foundational security concepts for Verify Get started with Telnyx Verify Real-time verification delivery status Brand your verification messages --- ## API Reference (Identity) ### Number Lookup - [Lookup phone number data](https://developers.telnyx.com/api-reference/number-lookup/lookup-phone-number-data.md): Returns information about the provided phone number. ### Verify - [Trigger Call verification](https://developers.telnyx.com/api-reference/verify/trigger-call-verification.md) - [Trigger Flash call verification](https://developers.telnyx.com/api-reference/verify/trigger-flash-call-verification.md) - [Trigger SMS verification](https://developers.telnyx.com/api-reference/verify/trigger-sms-verification.md) - [Trigger WhatsApp verification](https://developers.telnyx.com/api-reference/verify/trigger-whatsapp-verification.md) - [Retrieve verification](https://developers.telnyx.com/api-reference/verify/retrieve-verification.md) - [Verify verification code by ID](https://developers.telnyx.com/api-reference/verify/verify-verification-code-by-id.md) - [List verifications by phone number](https://developers.telnyx.com/api-reference/verify/list-verifications-by-phone-number.md) - [Verify verification code by phone number](https://developers.telnyx.com/api-reference/verify/verify-verification-code-by-phone-number.md) - [List all Verify profiles](https://developers.telnyx.com/api-reference/verify/list-all-verify-profiles.md): Gets a paginated list of Verify profiles. - [Create a Verify profile](https://developers.telnyx.com/api-reference/verify/create-a-verify-profile.md): Creates a new Verify profile to associate verifications with. - [Retrieve Verify profile](https://developers.telnyx.com/api-reference/verify/retrieve-verify-profile.md): Gets a single Verify profile. - [Update Verify profile](https://developers.telnyx.com/api-reference/verify/update-verify-profile.md) - [Delete Verify profile](https://developers.telnyx.com/api-reference/verify/delete-verify-profile.md) - [Retrieve Verify profile message templates](https://developers.telnyx.com/api-reference/verify/retrieve-verify-profile-message-templates.md): List all Verify profile message templates. - [Create message template](https://developers.telnyx.com/api-reference/verify/create-message-template.md): Create a new Verify profile message template. - [Update message template](https://developers.telnyx.com/api-reference/verify/update-message-template.md): Update an existing Verify profile message template.