Sending SMS with HTTP APIs
A website sends a text message by making an HTTP request to a provider. The premise is that simple, and almost none of the work is in the request.
The work is in the provider contract, consent trail, delivery status, retry behavior, and where the phone number and message body travel.
Think of the provider as a parcel carrier. The app prepares the shipment, the provider returns a tracking number, and later status callbacks report what happened in transit. The request is still HTTP, but the feature is an integration.
Provider API
Modern SMS providers expose an HTTP API for outbound messages. The exact fields differ by
provider, but the shape is familiar: authenticate, send a POST, receive a provider
message ID, then watch the status through callbacks or a status endpoint.
A vendor-neutral request has roughly this shape:
curl -X POST "https://api.sms-provider.example/messages" \
-H "Authorization: Bearer $SMS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "+15551234567",
"from": "CodeWalkers",
"body": "Your appointment is confirmed for 2:30pm."
}' Do not send that request from browser JavaScript. The API token belongs on the server, in a job worker, or behind a backend endpoint that validates the business action before the SMS provider sees anything.
There is no reason to revive raw socket code in PHP for ordinary SMS work. If the provider has a maintained SDK and it fits your stack, use it. If you use direct HTTP, keep the client small, typed, and boring enough that another developer can review every value being sent.
Model the Contract Explicitly
An SMS send call looks small until production starts asking concrete audit questions: who consented to the message, which sender ID was used, whether the provider accepted it, whether a carrier rejected it later, and what the user actually received.
Those answers form the shipment record around the provider's tracking number, so they should be part of the application model:
| Field | Why it matters |
|---|---|
| Recipient phone number | Normalize the number before sending and store only what the app needs. |
| Sender identity | The sender might be a long code, toll-free number, short code, alphanumeric sender ID, or provider messaging service. |
| Message template | A template key is easier to audit than free-form text spread across controllers. |
| Provider message ID | Store the ID returned by the provider so callbacks and support logs can be joined. |
| Delivery status | Accepted, pending, delivered, and rejected states do not mean the same thing, and each provider names them differently. |
| Consent record | Keep the opt-in source, timestamp, and purpose close to the send decision. |
The important point is that SMS delivery is not a boolean. A successful HTTP response means the provider accepted the request, not that a person read the text.
Keep the Adapter Server-side
The clean application shape is one SMS adapter with one public method. The rest of the app should not know whether the provider is a hosted API, an enterprise carrier gateway, or a test double.
<?php
final readonly class SmsMessage
{
public function __construct(
public string $to,
public string $sender,
public string $template,
public array $variables,
public string $idempotencyKey,
) {
}
}
final readonly class ProviderMessage
{
public function __construct(
public string $id,
public string $status,
) {
}
}
interface SmsGateway
{
public function send(SmsMessage $message): ProviderMessage;
} The readonly class modifier requires PHP 8.2 or newer. On PHP 8.1, use a
final class with typed properties and keep mutation out of the adapter contract.
That interface buys you two useful things. First, the provider credentials stay inside the adapter. Second, tests can assert that the application tried to send the right template without sending a real SMS at 3:00am because a test suite got enthusiastic.
The adapter is also the right place for provider-specific timeout settings, authentication, request signing, and response parsing. Put those details in controllers and they will spread. This vendor-neutral cURL adapter shows the complete HTTP boundary without naming a real service:
<?php
final readonly class CurlSmsGateway implements SmsGateway
{
public function __construct(private string $token)
{
}
public function send(SmsMessage $message): ProviderMessage
{
$handle = curl_init("https://api.example-sms.test/v1/messages");
if ($handle === false) {
throw new RuntimeException("Could not initialize the SMS request.");
}
$payload = json_encode([
"to" => $message->to,
"sender" => $message->sender,
"template" => $message->template,
"variables" => $message->variables,
], JSON_THROW_ON_ERROR);
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$this->token}",
"Content-Type: application/json",
"Idempotency-Key: {$message->idempotencyKey}",
],
CURLOPT_POSTFIELDS => $payload,
]);
try {
$response = curl_exec($handle);
if ($response === false) {
throw new RuntimeException("SMS request failed: " . curl_error($handle));
}
$statusCode = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if ($statusCode < 200 || $statusCode >= 300) {
throw new RuntimeException("SMS provider returned HTTP {$statusCode}.");
}
$data = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
if (
! is_array($data)
|| array_is_list($data)
|| ! is_string($data["id"] ?? null)
|| ! is_string($data["status"] ?? null)
) {
throw new RuntimeException("SMS provider returned an invalid response.");
}
return new ProviderMessage($data["id"], $data["status"]);
} finally {
curl_close($handle);
}
}
} This adapter requires PHP's cURL extension to run. Generate the idempotency key when the local notification record is created, then reuse that key for retries of the same intended message.
Consent and Registration
SMS has a tighter consent and deliverability story than a lot of old web tutorials admitted. For marketing or recurring notifications, the app needs a clear opt-in path, an opt-out path, and a record that explains why this phone number can be contacted.
Providers require verified sender registration in many countries. Check the provider and regulator's current documentation for the destination, sender type, message purpose, consent, and opt-out rules before enabling traffic.
Treat that as product work, not paperwork someone can add after launch. The message content, sender identity, opt-in language, help text, stop handling, and privacy policy all affect whether the integration is allowed to run cleanly.
Status Callbacks
The first response from the provider is only the beginning of the send. Like scans on a parcel route, status updates describe separate stages rather than one delivery boolean. Good SMS integrations listen for status callbacks or poll status when callbacks are not available. Treat every provider status as part of its documented contract because names and transitions differ.
Build the feature around that asynchronous path:
- Create a local notification record before calling the provider.
- Send the request with a short timeout and a clear idempotency plan.
- Store the provider message ID and initial status.
- Validate provider callbacks before trusting them.
- Update the local record as statuses arrive.
- Retry only when the failure mode is safe to retry.
The retry rule matters more than it looks. Retrying a network timeout is not the same as retrying a provider response that already accepted the message. If the app cannot tell those apart, it can send the same customer a duplicate alert. Store the local attempt before sending so support can trace the duplicate without relying on the message body.
Log Less than You Want
Phone numbers and message bodies are sensitive personal data. Keep them out of ordinary application logs regardless of which provider carries the message.
A safe application log usually contains these fields:
- The local notification ID.
- The provider message ID.
- A recipient hash or internal user ID.
- The template key.
- The provider status.
- A provider error code when one exists.
The unsafe log contains the full phone number, full message body, API token, callback signature secret, or a raw provider response copied into a permanent application log.
You will want the raw body during debugging. Build a short-lived, access-controlled debug path if the team truly needs it, then keep the ordinary logs boring.
Common Pitfalls & Debugging
The API Token Reaches Browser Code
Symptom: anyone who opens developer tools can copy the provider token. Cause: browser JavaScript calls the SMS API directly. Fix: keep credentials in the server-side adapter and let a validated application action request the send.
A Timeout Sends a Duplicate Message
Symptom: one account action sends the same alert twice. Cause: the first provider request succeeded, but the response timed out and the app retried blindly. Fix: persist a local attempt, use the provider's idempotency mechanism when available, and retry only failures known to be safe.
Callback Data Is Trusted Without Validation
Symptom: an untrusted request marks a message delivered or overwrites its error code. Cause: the callback route accepts fields without verifying the provider signature. Fix: validate the signature with the provider SDK, then accept unknown extra fields so provider additions do not break the handler.
Frequently Asked Questions
Does a successful API response mean the SMS was delivered?
A successful API response does not prove SMS delivery. It usually means the provider accepted or queued the message. Delivery happens later through carrier networks, so store the provider message ID and update the local record from validated status callbacks.
Should PHP send SMS inside the browser request?
A low-volume action can call the provider after validation, but a queued job is safer when delays or retries would hold the browser request open. Record the intended notification before the worker sends it.
Which phone number format should an SMS API use?
Use the international or national format required by the provider and destination country. Normalize and validate the number before it reaches the provider adapter, but keep provider-specific formatting rules inside that integration boundary.
What should an SMS application log?
Log the local notification ID, provider message ID, internal user ID or recipient hash, template key, status, and provider error code. Keep full phone numbers, message bodies, tokens, and callback secrets out of ordinary logs.
Does sending SMS in the US require carrier registration before a provider will deliver it?
Application-to-person SMS in the United States generally requires the sending number and use case to be registered and vetted with carriers before a provider delivers messages at volume. Registration programs change over time, so confirm the provider's current process before launch rather than assuming an unregistered number will deliver reliably.
Next Steps
A web app asks a remote service to send a message, and the provider message ID is the tracking number that joins the local record to later status updates. Keep the implementation in a provider adapter with consent and retry rules.
If you are exposing your own endpoint first, read build your own API with PHP. If the provider contract is older and XML-shaped, read the SOAP adapter section in the PHP API capstone. If the SMS sends from a form or account action, pair the integration with PHP security fundamentals before a real phone number enters the system.
This example has been updated from the original CodeWalkers SMS article, which posted to a gateway and stopped at the HTTP response. Carrier registration, consent records, and delivery callbacks are now part of the same feature, which is why the adapter here returns a message ID rather than a boolean.
Sources
-
[1]
Overview of HTTP(developer.mozilla.org)
-
[2]
PHP Classes and Objects(php.net)
-
[3]
cURL(php.net)
-
[4]
curl_setopt_array(php.net)
-
[5]
json_decode(php.net)
Read Next
Finish the PHP web-app path with a small JSON API, HTTP methods, status codes, validation, PDO, and an OpenAPI-ready contract.
Build a PHP endpoint that tells the browser when data changed, returns small JSON deltas, and avoids full-page refreshes for chat boxes, dashboards, and status panels.
Modern PHP reference guides for frameworks, PDO, configuration, pagination, conditionals, forms, files, sockets, media streaming, mini chat apps, XML, templates, OOP, APIs, email, SOAP, recursion, SQL-backed apps, and security fundamentals.