Send SMS from PHP with an SMS API or a notification API, with working code for each. Plus why carrier email-to-SMS gateways are no longer worth building on.
Updated Sep 11, 2026
Last updated: September 2026. Package versions verified against Packagist on 2026-09-11.
In short: use an SMS API if text is the only channel you will ever need, and put a notification API in front of it if SMS is one channel among several. Carrier email-to-SMS gateways used to be the cheap shortcut, and they are being switched off.
PHP cannot reach the cellular network on its own, so sending an SMS means handing the message to someone who can. There are three ways to do that, and they differ by how much of the surrounding machinery you end up owning.
The old trick was that each US carrier ran an email-to-SMS bridge, so mailing 5555551234@vtext.com reached a Verizon subscriber's phone as a text. No API key, no provider account, no cost. You could do it with PHP's mail() in one line.
That era is ending:
txt.att.net and mms.att.net.vtext.com and vzwpix.com, and has announced an end date.tmomail.net is effectively discontinued, and Cricket's gateway is already gone.Check your carrier's own support pages for current status before relying on any of them.
The reason is straightforward. Anyone on the internet could email any number at these domains with no sender verification, which made them a free phishing channel at scale. Meanwhile US carriers moved to requiring registered senders through 10DLC, and an anonymous email bridge cannot satisfy that.
Even while a gateway is technically alive, messages through it are increasingly filtered or dropped without a bounce. That is the worst failure mode a notification can have: your code returns success, your logs look clean, and the user never hears from you.
There were always real limits too. You had to know each recipient's carrier, which you cannot reliably determine and which changes when someone ports their number. There was no delivery status, no sender identity beyond "some email address," and no way to handle replies.
What to do instead: if you are maintaining something that currently emails a carrier gateway, treat it as a live outage risk rather than technical debt, and move it to an SMS API. The code below is about the same length.
An SMS API gives you a provider account, a sender identity, real delivery receipts, and support when something breaks. MessageBird, Twilio, and Plivo all cover PHP. The example below uses MessageBird, whose PHP SDK is actively maintained.
What you need first
Install
composer require messagebird/php-rest-api
Verified against messagebird/php-rest-api v4.0.1.
Send the message
<?phprequire __DIR__ . '/vendor/autoload.php';$client = new \MessageBird\Client(getenv('MESSAGEBIRD_API_KEY'));$message = new \MessageBird\Objects\Message();$message->originator = 'AcmeInc';$message->recipients = ['+15558675309'];$message->body = 'Your order has shipped.';try {$response = $client->messages->create($message);echo $response->getId(), PHP_EOL;} catch (\MessageBird\Exceptions\AuthenticateException $e) {fwrite(STDERR, "Bad API key\n");} catch (\Exception $e) {fwrite(STDERR, 'Send failed: ' . $e->getMessage() . PHP_EOL);}
One statement per line matters here more than it looks. PHP's // comment runs to the end of the line, so joining several statements onto one line after a comment silently discards all of them and the script still exits cleanly.
recipients is an array, so the same call sends to several numbers. Every number should be in E.164 format, meaning a leading + and country code. Do not store numbers as PHP integers; 5555551234 loses the + and any leading zero.
Where it gets thin: message copy lives in your source, so a wording change is a deploy. Delivery receipts arrive as webhooks you store and interpret. Opt-outs are your responsibility once someone replies STOP. And if you later need email or push, that is a separate integration.
The SMS API above solves delivery. Here is what usually turns up in the fortnight afterward:
A notification API such as Courier sits above your SMS provider and handles those. You keep MessageBird, Twilio, or Plivo delivering, and your PHP stops knowing which one it is.
What the extra layer gives you:
Set it up
Create a free account: the Developer plan includes 10,000 messages a month, and past that it's $0.005 per message, the same rate on every channel. Then connect an SMS provider under Channels using the credentials that provider already gave you. This is the same MessageBird or Twilio account from the section above. You are not changing who delivers the SMS.
Install
composer require trycourier/courier
Verified against trycourier/courier v7.4.0, which requires PHP 8.1 or newer. Note the package name: the SDK's own README still shows composer require trycourier/courier-php, but trycourier/courier is the one that resolves on Packagist. Pin the major version rather than floating, since this SDK moved from v5 to v7 in about a month.
Send the message
<?phprequire __DIR__ . '/vendor/autoload.php';use Courier\Client;$client = new Client(apiKey: getenv('COURIER_API_KEY'));$result = $client->send->message(message: ['to' => ['phone_number' => '+15558675309'],'content' => ['title' => 'Order update','body' => 'Hi Erika, your order has shipped.',],'routing' => ['method' => 'single', 'channels' => ['sms']],],);echo $result->requestId, PHP_EOL;
That sends content defined inline. The version you will run in production references a template you published in Courier and addresses the user rather than a raw number:
$result = $client->send->message(message: ['to' => ['user_id' => 'user-123'],'template' => 'ORDER_SHIPPED','data' => ['name' => 'Erika','tracking_url' => 'https://example.com/t/abc',],],);
Nothing in that call names SMS. Routing and the user's saved preferences decide the channel, so moving a notification to push, or sending both, is configuration rather than a code change.
The SDK uses named arguments, which is why it needs PHP 8.1. The returned requestId is what you search on in the Courier logs to see queued, sent, delivered, and undeliverable states along with any provider error.
Where it gets thin: one more service in the path, and one more concept before your first send. It is also the wrong tool for two-way SMS conversations, which belong with your provider's inbound APIs.
If you are writing this in Cursor, Claude Code, or Codex, connect the agent to Courier directly instead of pasting snippets. It then works from the current SDK shapes rather than the 2022-era API that most training data still contains, which is the difference between PHP that compiles and PHP that does not.
The MCP server gives the agent typed tool calls. In Claude Code:
claude mcp add --transport http courier https://mcp.courier.com --header api_key:YOUR_API_KEY
Courier Skills adds the practices that are easy to get wrong, such as never batching a one-time passcode:
npx skills add trycourier/courier-skills
Both sit alongside the Courier CLI for scripting and CI. courier.com/agents collects the setup for Cursor, Claude Code, Codex, and VS Code in one place, and Build with AI has the machine-readable docs index an agent can fetch on its own.
Then ask in plain language: "send an SMS when an order ships, and fall back to email if it fails." The agent picks the right primitive, and knows which channels need a recorded opt-in.
This matters for the same reason this guide version-stamps every snippet. An agent working from stale training data will confidently write a send call that no longer exists.
| Email-to-SMS gateway | SMS API | Notification API | |
|---|---|---|---|
| Package | none, mail() | messagebird/php-rest-api v4.0.1 | trycourier/courier v7.4.0 |
| Lines to first send | ~1 | ~10 | ~12 |
| Cost per message | free | per message | $0.005 per message, first 10,000 a month free |
| Delivery receipts | none | webhooks you handle | built in, one log |
| Needs recipient's carrier | yes | no | no |
| Templates outside your code | no | no | yes |
| Preferences and opt-outs | no | you build it | built in |
| Provider failover | n/a | n/a | yes, across providers |
| Still supported by carriers | being shut down | yes | yes |
| Best for | nothing new | SMS-only production apps | SMS inside a product flow |
If SMS is the only channel you will ever send, an SMS API is the whole answer and you can stop at section 2. If you can already name a second channel you will need, the notification API is worth the extra hop, because the alternative is writing preferences, failover, and template management yourself.
FAQ
Keep exploring
One API, every channel
Courier gives you one API for email, SMS, push, and chat, with templates, routing, retries, and delivery logs built in.
Last updated Sep 11, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.
© 2026 Courier. All rights reserved.