PHPackages                             edenohana/sms-free-php - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. edenohana/sms-free-php

ActiveLibrary

edenohana/sms-free-php
======================

A modern, typed PHP client for the SMS4Free (sms4free.co.il) HTTP API: SMS sending, Israeli phone number handling and OTP generation.

v2.2.0(today)11↑2900%1MITPHPPHP &gt;=8.1CI passing

Since Aug 14Pushed today1 watchersCompare

[ Source](https://github.com/EdenOhanaProgramming/smsFreePHP)[ Packagist](https://packagist.org/packages/edenohana/sms-free-php)[ Docs](https://github.com/EdenOhanaProgramming/smsFreePHP)[ RSS](/packages/edenohana-sms-free-php/feed)WikiDiscussions main Synced today

READMEChangelog (3)Dependencies (5)Versions (4)Used By (0)

smsFreePHP
==========

[](#smsfreephp)

[![CI](https://github.com/EdenOhanaProgramming/smsFreePHP/actions/workflows/ci.yml/badge.svg)](https://github.com/EdenOhanaProgramming/smsFreePHP/actions/workflows/ci.yml)[![PHP](https://camo.githubusercontent.com/69ada8118f91b7cbf415af2f0d9f7a21ad3fe896d8dad3109d8b3d7c6c275941/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253345253344382e312d373737626234)](https://www.php.net/supported-versions)[![License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)](LICENSE)

A modern, typed PHP client for the [SMS4Free](https://www.sms4free.co.il/) HTTP API: sending SMS, handling Israeli phone numbers, and generating one-time passcodes.

**🇮🇱 [README בעברית](README.he.md)** | [API reference](docs/api-reference.md) | [Laravel](docs/laravel.md) | [Upgrading from 1.x](UPGRADING.md)

---

What this solves
----------------

[](#what-this-solves)

Talking to the SMS4Free API is one `curl` call. Everything around it is where the work actually is.

Phone numbers arrive messy. `054-123-4567`, `+972 54 123 4567` and `00972541234567` are the same line, so they all get parsed into one canonical form, and anything that isn't a real Israeli mobile number is skipped and reported, so the rest of the list still goes out.

Hebrew breaks naive string handling. Cutting a message with `substr()` splits a two-byte character in half, and counting with `strlen()` reports bytes rather than characters. Everything here is multibyte-safe.

Failures need to be told apart. "The number is invalid", "the provider says you're out of balance" and "the network is down" call for three different reactions in your application, so each one is a different exception type.

And credentials are secrets: they never reach an exception message, and they're redacted from `var_dump()` output.

Requirements
------------

[](#requirements)

PHP8.1 or newerExtensions`curl`, `json`, `mbstring`AccountAn [SMS4Free](https://www.sms4free.co.il/) account: username, password and API keyInstallation
------------

[](#installation)

```
composer require edenohana/sms-free-php
```

Not using Composer? Copy the folder into your project and require the bundled autoloader:

```
require_once __DIR__ . '/smsFreePHP/src/autoload.php';
```

Quick start
-----------

[](#quick-start)

```
use EdenOhana\SmsFree\Credentials;
use EdenOhana\SmsFree\Sms4FreeClient;

$client = new Sms4FreeClient(new Credentials('username', 'password', 'api-key'));

$result = $client->send(
    senderName: 'MyShop',            // a verified sender number, or an approved sender ID
    recipients: ['054-123-4567'],    // one number or a list
    message:    'ההזמנה שלך יצאה לדרך',
);

echo $result->acceptedCount(); // 1
```

Keep secrets out of the source tree by reading them from the environment:

```
$client = new Sms4FreeClient(Credentials::fromEnvironment());
// reads SMS4FREE_USERNAME, SMS4FREE_PASSWORD and SMS4FREE_API_KEY
```

Handling failures
-----------------

[](#handling-failures)

`send()` either returns a [`SendResult`](src/SendResult.php) or throws. Every exception the library raises implements `SmsFreeException`, so a single `catch` is enough when you don't care about the difference:

```
use EdenOhana\SmsFree\Exception\ApiException;
use EdenOhana\SmsFree\Exception\InvalidPhoneNumberException;
use EdenOhana\SmsFree\Exception\SmsFreeException;
use EdenOhana\SmsFree\Exception\TransportException;

try {
    $client->send('MyShop', $recipients, $text);
} catch (InvalidPhoneNumberException $e) {
    // Bad user input. Nothing was sent, nothing was charged.
    $form->addError('phone', implode(', ', $e->invalidNumbers()));
} catch (ApiException $e) {
    // The provider refused: wrong credentials, no balance, unverified sender.
    $logger->error('SMS4Free refused', ['status' => $e->status(), 'reason' => $e->providerMessage()]);
} catch (TransportException $e) {
    // Network trouble. The message may or may not have gone out, see the note on retries below.
    $logger->warning('SMS4Free unreachable', ['error' => $e->getMessage()]);
} catch (SmsFreeException $e) {
    // Anything else from this library.
}
```

ExceptionMeaningWas a credit spent?`InvalidArgumentException`Empty sender, empty recipient list, empty body, message over the limitNo, the request is never made`InvalidPhoneNumberException`One or more recipients could not be parsedNo`TransportException`Timeout, DNS or TLS failure, non-2xx status, unreadable bodyUnknown`ApiException`The provider answered with a non-positive statusDepends on the providerValidating before you send
--------------------------

[](#validating-before-you-send)

Checking numbers costs nothing, so validate the form first and only then spend a credit:

```
$invalid = $client->findInvalidRecipients($rowsFromCsv);

if ($invalid !== []) {
    throw new RuntimeException('Unusable numbers: ' . implode(', ', $invalid));
}
```

### One bad number in a list of five hundred

[](#one-bad-number-in-a-list-of-five-hundred)

By default an unparseable recipient is skipped: the message goes to everyone the library can parse, and the rest come back from `SendResult::skippedRecipients()`. A send to a single invalid number still throws, because there is nobody left to send to.

```
$result = $client->send('MyShop', $rowsFromCsv, $text);

if ($result->hasSkippedRecipients()) {
    $logger->warning('Left out of the send', ['numbers' => $result->skippedRecipients()]);
}
```

The skipped values come back exactly as they were supplied, so they can go straight into a report for whoever owns the list. A send where *no* recipient survives still throws, because delivering to nobody is never what the caller meant.

If you prefer one bad number to block the whole request (useful for OTP flows where the single recipient must be valid), switch the policy:

```
use EdenOhana\SmsFree\ClientOptions;
use EdenOhana\SmsFree\InvalidRecipientPolicy;

$client = new Sms4FreeClient(
    Credentials::fromEnvironment(),
    (new ClientOptions())->withInvalidRecipientPolicy(InvalidRecipientPolicy::RejectRequest),
);
```

Or work with the value object directly:

```
use EdenOhana\SmsFree\PhoneNumber;

$number = PhoneNumber::parse('054-123-4567');

$number->national(); // '0541234567', what the provider is given
$number->e164();     // '+972541234567', what you want in your database
$number->raw();      // '054-123-4567', what the user typed
```

Message length, Hebrew and credits
----------------------------------

[](#message-length-hebrew-and-credits)

A Hebrew message is carried as UCS-2, which fits **70 characters per SMS part** instead of the 160 a Latin message gets. That's the most common billing surprise with this provider, so the library makes it visible:

```
use EdenOhana\SmsFree\Message;

$message = Message::of('הקוד שלך לאימות הוא 123456');

$message->encoding();  // SmsEncoding::Ucs2
$message->length();    // 26 characters
$message->parts();     // 1, how many messages the account is billed for
```

SMS4Free accepts up to 134 characters per request. By default a longer body is shortened on a character boundary and the result tells you it happened:

```
$result = $client->send('MyShop', $recipients, $veryLongText);

if ($result->wasTruncated()) {
    $logger->notice('The message was shortened before sending.');
}
```

If losing the tail of a message is unacceptable (a link at the end, for instance), turn truncation into a hard failure:

```
use EdenOhana\SmsFree\ClientOptions;

$client = new Sms4FreeClient(
    Credentials::fromEnvironment(),
    (new ClientOptions())->withMessageTruncation(false),
);
```

One-time passcodes
------------------

[](#one-time-passcodes)

```
use EdenOhana\SmsFree\Otp\OtpGenerator;

$code = (new OtpGenerator(length: 6))->generate(); // '042317', a string, so leading zeros survive

$client->send('MyShop', [$phone], "הקוד שלך לאימות הוא: {$code}");

// Later, when the user types it back:
OtpGenerator::matches($storedCode, $typedCode); // constant-time comparison
```

Codes come from `random_int()`, PHP's cryptographically secure generator. Store the code hashed with an expiry and an attempt limit. [`examples/send-otp.php`](examples/send-otp.php) shows the whole flow.

Configuration
-------------

[](#configuration)

```
use EdenOhana\SmsFree\ClientOptions;

$options = (new ClientOptions())
    ->withTimeouts(connectTimeout: 3.0, timeout: 10.0)
    ->withMessageTruncation(false)
    ->withInternationalRecipients(true)  // accept non-Israeli numbers
    ->withInvalidRecipientPolicy(InvalidRecipientPolicy::SkipInvalid)
    ->withMaxMessageLength(70)
    ->withUserAgent('my-app/2.1')
    ->withCaBundlePath('/etc/ssl/certs/cacert.pem'); // for hosts with no CA store

$client = new Sms4FreeClient(Credentials::fromEnvironment(), $options);
```

Defaults: a 5 second connect timeout and a 15 second overall timeout, truncation on, Israeli recipients only, unparseable recipients skipped, and TLS verification always on.

### A note on retries

[](#a-note-on-retries)

The library does not retry automatically. A timeout doesn't tell you whether the provider received the request, since the answer may simply have been lost on the way back, and an automatic retry can quietly send the same message twice and bill you twice. If you want retries, add them where you have the context to make them safe: a queue with an idempotency key, for example.

Using a different HTTP stack
----------------------------

[](#using-a-different-http-stack)

The transport sits behind [`HttpClient`](src/Http/HttpClient.php). Implement it to route requests through Guzzle, Symfony HttpClient, a PSR-18 client, or a fake in your own tests:

```
final class GuzzleTransport implements HttpClient
{
    public function post(string $url, string $body, array $headers = []): HttpResponse
    {
        // ...
    }
}

$client = new Sms4FreeClient($credentials, new ClientOptions(), new GuzzleTransport());
```

Laravel
-------

[](#laravel)

The package ships a service provider, a facade and a notification channel, discovered automatically by Laravel 11 and 12. Fill in `.env` and you can send:

```
// Notification
public function via(object $notifiable): array
{
    return ['sms4free'];
}

public function toSms4Free(object $notifiable): string
{
    return "הקוד שלך לאימות הוא: {$this->code}";
}
```

[docs/laravel.md](docs/laravel.md) covers the config file, where the channel looks for a phone number, queued notifications and testing.

Upgrading from 1.x
------------------

[](#upgrading-from-1x)

The old `SMSService` class still ships and still behaves exactly as it did, so upgrading the package changes nothing until you're ready. It's deprecated and will be removed in 3.0. [UPGRADING.md](UPGRADING.md) is a short read.

Development
-----------

[](#development)

```
composer install
composer test      # PHPUnit
composer analyse   # PHPStan, level 9
composer cs        # coding standards (composer cs:fix to apply)
composer check     # all three
```

Contributing
------------

[](#contributing)

Bug reports and pull requests are welcome, see [CONTRIBUTING.md](CONTRIBUTING.md). Found a security issue? Please follow [SECURITY.md](SECURITY.md) instead of opening a public issue.

Legal
-----

[](#legal)

The MIT licence covers the code, including its "as is" disclaimer: no warranty, and no liability for what happens when you use it. Two things it does not cover, both of which sit with whoever sends the messages.

**Israeli anti-spam law.** Amendment 40 to the Communications (Telecommunications and Broadcasting) Law requires explicit prior consent before an advertising message is sent, an identifiable sender, and a working way to opt out. Statutory damages reach ₪1,000 per message without the recipient having to prove any loss, so a careless bulk send gets expensive quickly. A transactional message, such as a verification code or a delivery update for an order the person placed, is a different matter from marketing.

**The provider's terms.** This is an unofficial client. Your account is governed by SMS4Free's own terms, and nothing in this package changes what they permit.

smsFreePHP is not affiliated with, endorsed by, or connected to SMS4Free. The name is used only to say which API the library speaks to. None of the above is legal advice.

License
-------

[](#license)

[MIT](LICENSE) © Eden Ohana

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance100

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 92.6% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/174742207?v=4)[EdenOhanaProgramming](/maintainers/EdenOhanaProgramming)[@EdenOhanaProgramming](https://github.com/EdenOhanaProgramming)

---

Top Contributors

[![EdenOhanaProgramming](https://avatars.githubusercontent.com/u/174742207?v=4)](https://github.com/EdenOhanaProgramming "EdenOhanaProgramming (25 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (2 commits)")

---

Tags

phplaravelotpnotificationssmshebrewnotification-channelIsraelsms4free

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/edenohana-sms-free-php/health.svg)

```
[![Health](https://phpackages.com/badges/edenohana-sms-free-php/health.svg)](https://phpackages.com/packages/edenohana-sms-free-php)
```

###  Alternatives

[ferdous/laravel-otp-validate

Laravel package for OTP validation with built-in features like retry and resend mechanism. Built in max retry and max resend blocking. OTP/Security Code can be send over SMS or Email of your choice with user-defined template.

7124.7k](/packages/ferdous-laravel-otp-validate)[craftsys/msg91-laravel

Laravel service provider for Msg91 apis to Send OTPs, Verify OTPs, Resend OTPs, Send SMS (Short Message) etc

12107.0k2](/packages/craftsys-msg91-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
