Send SMS with PHP and HTTP APIs
A PHP application sends SMS through a provider’s SDK or HTTP API. You need server-side credentials, an approved sender and a recipient number. The provider accepts the request first; delivery status arrives later.
Start with one test message, then add delivery tracking and retries. Keep consent records and credentials out of public code.
How Do You Send an SMS with PHP?
Use an SMS provider’s PHP SDK to send a recipient number, sender number and message body. For Twilio, install twilio/sdk with Composer, configure your account credentials and use an SMS-capable sender. Twilio’s current trial flow requires a verified recipient; its legacy Console also offers a Virtual Phone for testing.
composer require twilio/sdk Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER and SMS_TO_NUMBER in your local environment. Use a recipient you have permission to message. Save this beside the Composer vendor directory:
<?php
require __DIR__ . "/vendor/autoload.php";
$client = new Twilio\Rest\Client(
getenv("TWILIO_ACCOUNT_SID"),
getenv("TWILIO_AUTH_TOKEN")
);
$message = $client->messages->create(
getenv("SMS_TO_NUMBER"),
[
"from" => getenv("TWILIO_FROM_NUMBER"),
"body" => "Your appointment is confirmed for 2:30pm.",
]
);
echo $message->sid, PHP_EOL; A returned message SID identifies the request; it does not prove delivery. Handle SDK errors and use the provider’s delivery status before reporting success to a user. Use restricted API credentials for a deployed application where supported.
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.
The following request illustrates the HTTP fields. The example domain is fictional; use your provider’s documented endpoint and payload:
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.
What Should a PHP App Store for Each SMS?
A PHP app should store the intended recipient, sender, template, consent record, provider message ID and delivery status. These let you check what was requested and what happened afterward.
Keep these fields with the notification record:
| 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. |
SMS delivery has more than two states. A successful HTTP response only means the provider accepted the request.
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 cURL adapter is a design example for a fictional provider, not a drop-in Twilio client. Replace the endpoint, authentication and response fields with your provider’s documented API:
<?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 and reuse it for retries only if the provider supports idempotent requests. A custom header alone cannot prevent duplicate messages.
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 to finish before 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.
Retrying a network timeout is different from retrying a request the provider already accepted. 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
Anyone who opens developer tools can copy the provider token. This happens when browser JavaScript calls the SMS API directly. Keep credentials in the server-side adapter and let a validated application action request the send.
A Timeout Sends a Duplicate Message
One account action can send the same alert twice if the provider accepts a request but the response times out. Record the attempt, use the provider’s idempotency mechanism when available, and check the original request before retrying an uncertain outcome.
Callback Data Is Trusted Without Validation
An untrusted request marks a message delivered or overwrites its error code. The callback route accepts fields without verifying the provider signature. 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 PHP SMS handler can call the provider directly for a low-volume action 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?
PHP SMS integrations should format recipient numbers as 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 the provider integration.
What should an SMS application log?
PHP SMS logs should record 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.
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.
A beginner's introduction to PHP: whether it is worth learning in 2026, what runs on it, PHP or Node, whether it is secure, how coding agents change the work, and a roadmap to every PHP guide.