Before you start
- An API connection created in the web panel (app.grupotecnophone.com), with its sandbox or production Bearer Token.
- Your server’s public IP registered as authorized for that token.
- A User-Agent that identifies your application, for example MyCompany/1.0.
- .NET 6 or later. The examples use HttpClient and System.Text.Json, which are built in: no NuGet packages required.
Store the token in an environment variable (GTP_SMS_TOKEN in these examples) or in your platform’s secret store, and never ship it in a mobile, desktop or frontend app: API calls must always come from your backend, from an authorized IP.
Send an SMS with C# and HttpClient
The following console program sends an SMS and processes the response:
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var token = Environment.GetEnvironmentVariable("GTP_SMS_TOKEN")
?? throw new InvalidOperationException("GTP_SMS_TOKEN is not set");
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.UserAgent.ParseAdd("MyCompany/1.0");
var payload = new
{
to = "+525512345678",
body = "Your verification code is 482913. It expires in 5 minutes."
};
using var response = await http.PostAsJsonAsync(
"https://api.grupotecnophone.com/prod/v1/sms/send", payload);
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (response.IsSuccessStatusCode
&& root.TryGetProperty("status", out var status)
&& status.GetString() == "success")
{
Console.WriteLine($"Sent. SID: {root.GetProperty("sid").GetString()} - segments: {root.GetProperty("num_segments").GetInt32()}");
}
else
{
var code = root.TryGetProperty("error", out var error) && error.ValueKind == JsonValueKind.Object
? error.GetProperty("code").GetString()
: $"HTTP_{(int)response.StatusCode}";
Console.WriteLine($"Not sent ({code}): {json}");
}
API response
If the message was accepted, the API responds with HTTP 200 and a JSON body like this:
{
"sid": "sms_20261012143201_39bdafbd",
"status": "success",
"to": "+525512345678",
"body": "Your verification code is 482913. It expires in 5 minutes.",
"encoding": "GSM7",
"num_chars": 56,
"num_segments": 1,
"error": null
}
Store the sid: it is the identifier you will use to reconcile the message’s delivery report. The num_segments field indicates how many segments will be billed; see how segments are calculated.
Send SMS to multiple recipients with C#
To send bulk SMS from .NET, wrap the call in a class and reuse the same HttpClient. That way you keep the sid of each message and separate the errors.
using System.Net.Http.Json;
using System.Text.Json;
public sealed record SmsResult(bool Ok, string? Sid, string? Error);
public sealed class GtpSmsClient
{
private const string Endpoint = "https://api.grupotecnophone.com/prod/v1/sms/send";
private readonly HttpClient _http;
// Pass in an HttpClient already configured with the token, User-Agent and timeout
public GtpSmsClient(HttpClient http) => _http = http;
public async Task<SmsResult> SendAsync(string to, string body, CancellationToken ct = default)
{
using var response = await _http.PostAsJsonAsync(Endpoint, new { to, body }, ct);
var json = await response.Content.ReadAsStringAsync(ct);
try
{
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (response.IsSuccessStatusCode
&& root.TryGetProperty("status", out var s) && s.GetString() == "success")
{
return new SmsResult(true, root.GetProperty("sid").GetString(), null);
}
var code = root.TryGetProperty("error", out var e) && e.ValueKind == JsonValueKind.Object
? e.GetProperty("code").GetString()
: null;
return new SmsResult(false, null, code ?? $"HTTP_{(int)response.StatusCode}");
}
catch (JsonException)
{
return new SmsResult(false, null, $"HTTP_{(int)response.StatusCode}");
}
}
}
// Usage: go through the list at a moderate pace and keep the sid of each message
var sms = new GtpSmsClient(http);
string[] recipients = { "+525512345678", "+528112345678" };
foreach (var number in recipients)
{
var r = await sms.SendAsync(number, "Your order has shipped. Thank you for your purchase.");
Console.WriteLine(r.Ok ? $"{number}: {r.Sid}" : $"{number}: error {r.Error}");
await Task.Delay(200);
}
In ASP.NET Core, register the class with builder.Services.AddHttpClient<GtpSmsClient>() and configure the token, User-Agent and timeout there. For high volumes, queue the sends in a background service. Without coding, you can also send bulk SMS from Excel or CSV.
Send an OTP code with C#
Generate the code with RandomNumberGenerator, which is cryptographically secure, store only its hash and compare it in constant time.
using System.Security.Cryptography;
using System.Text;
// 6-digit code from a cryptographically secure generator
var code = RandomNumberGenerator.GetInt32(0, 1_000_000).ToString("D6");
// Store only the hash, with an expiry and a maximum number of attempts
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{userId}:{code}"));
await repository.SaveOtpAsync(userId, hash, DateTimeOffset.UtcNow.AddMinutes(5));
var r = await sms.SendAsync(phone,
$"Your verification code is {code}. It expires in 5 minutes. Do not share it.");
// When validating, compare the hashes in constant time
bool IsValid(byte[] stored, string entered) =>
CryptographicOperations.FixedTimeEquals(
stored, SHA256.HashData(Encoding.UTF8.GetBytes($"{userId}:{entered}")));
More on the flow in the SMS OTP authentication guide and in our SMS OTP service for businesses.
Common errors
| HTTP | Code | Meaning |
|---|---|---|
| 400 | ENCRYPTION_REQUIRED | The connection requires to and body to be encrypted with RSA-OAEP |
| 400 | ENCRYPTION_NOT_ALLOWED | The connection operates in plain text and encrypted content was received |
| 400 | INPUT_PHONE_BLACKLISTED | The destination number is on the blacklist |
| 400 | INSUFFICIENT_BALANCE | Insufficient balance for the send |
| 401 | — | Missing or invalid token |
| 403 | — | The source IP is not authorized for the token |
| 500 | — | Unexpected internal error |
On 400 errors the response includes an error object with code, code_num and message.
Test in sandbox
Change the endpoint to https://api.grupotecnophone.com/test/v1/sms/send and use the sandbox token. That environment validates the token, IP, payload and encryption with the same logic as production, but does not send real SMS.
Best practices
- Set a timeout and log each response with its
sid. - After a network error with no response, do not resend blindly: check first so you don’t duplicate the message to the user.
- Retry only 500 errors, with progressive backoff and a maximum number of attempts.
- Queue bulk sends instead of sending them within a web request cycle.
- If the content is sensitive, enable connection encryption; the API documentation describes the RSA-OAEP encryption format.
See also the SMS API for Mexico, the SMPP connection and the guides for PHP, Laravel, Python, Node.js, Java and cURL. If you need test credentials, contact us.


