PHPackages                             everesthome/sendify - 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. everesthome/sendify

ActiveLibrary

everesthome/sendify
===================

PHP client and Laravel integration for the Sendify WhatsApp service

v1.1.2(today)18↑37.5%MITPHPPHP ^8.2CI passing

Since Aug 16Pushed todayCompare

[ Source](https://github.com/everesthome/sendify-php)[ Packagist](https://packagist.org/packages/everesthome/sendify)[ Docs](https://github.com/everesthome/sendify-php)[ RSS](/packages/everesthome-sendify/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (8)Versions (6)Used By (0)

Sendify for PHP and Laravel
===========================

[](#sendify-for-php-and-laravel)

[![Packagist](https://camo.githubusercontent.com/d838dcf3d2c155875035279ef8e97573fb8001003c519ef343419c87866a916b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f65766572657374686f6d652f73656e646966792e737667)](https://packagist.org/packages/everesthome/sendify)[![Tests](https://github.com/everesthome/sendify-php/actions/workflows/run-tests.yml/badge.svg)](https://github.com/everesthome/sendify-php/actions/workflows/run-tests.yml)[![Downloads](https://camo.githubusercontent.com/ed00339ba5e8851cf9f4896e0376cd444e28fa0360b7f01a3c3e61f073ac3ad9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f65766572657374686f6d652f73656e646966792e737667)](https://packagist.org/packages/everesthome/sendify)[![License](https://camo.githubusercontent.com/7aa6f60b4ccd22ecdb9737b34fc73c44c2d6fdfba7ba96204894f46dfec3c54d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f65766572657374686f6d652f73656e646966792e737667)](LICENSE.md)

Client for the Sendify service: send WhatsApp messages from any PHP application. On Laravel you install it, add three keys to your `.env`, and you are done:

```
Sendify::TextMessageTo('5215551234567', 'Hello from Laravel');
```

The core is framework-agnostic — it only needs cURL and JSON.

**Package:**

Table of contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Sending messages](#sending-messages)
    - [Media](#media)
    - [Responses](#responses)
    - [Bulk sending](#bulk-sending)
    - [Message actions](#message-actions)
- [Instance status](#instance-status)
- [Instance lifecycle](#instance-lifecycle)
- [Webhooks, templates, automations, profile and statuses](#webhooks-templates-automations-profile-and-statuses)
- [Multiple instances or servers](#multiple-instances-or-servers)
- [Error handling](#error-handling)
- [Using it without Laravel](#using-it-without-laravel)
- [Testing](#testing)
- [License](#license)

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

[](#requirements)

RequirementVersionPHP8.2, 8.3 or 8.4Extensions`ext-curl`, `ext-json`Laravel10, 11, 12 or 13 (optional — only for the facade and the service provider)Installation
------------

[](#installation)

```
composer require everesthome/sendify
```

LLM Prompt:

```
# Task: Integrate Sendify WhatsApp notifications (Laravel)

Add WhatsApp notifications via https://github.com/everesthome/sendify-php when
tasks are created, reassigned, and completed.

## Install
1. `composer require everesthome/sendify`
2. Add EXACTLY these three vars to `.env` and `.env.example` — nothing else:
   SENDIFY_URL, SENDIFY_CLIENT, SENDIFY_INSTANCE
3. Do NOT run `vendor:publish`. The package ships its own config and reads the
   env directly. Do NOT create config files or config toggles (enabled flags,
   per-event switches, queue names, country-code settings). Hardcode constants
   in the service class instead.

## Architecture — 2 files, plus 2 lines in the model
Create ONLY:
- `app/Services/TaskWhatsAppNotifier.php` — builds the text and sends it
- `app/Observers/TaskObserver.php` — decides when to fire

Register with `#[ObservedBy(TaskObserver::class)]` on the model. Use a model
observer, NOT edits to controllers: entities are usually created from several
places (multiple panels/roles, series controllers, scheduled commands) and all
of them must notify identically. Verify this by grepping for every `Model::create`
call site before you start.

## Send immediately — no queue
Call `Sendify::textMessageTo()` directly inside the request. Do NOT create a Job,
do NOT use `dispatch()`, do NOT touch queue config. A queued job silently does
nothing until a worker runs, which reads as "the API is broken".

Wrap the call in `try { } catch (SendifyException $e) { Log::warning(...); }` so a
WhatsApp failure never breaks the save. Note the tradeoff in
save now waits on the HTTP call (package default timeout 30s).

## Observer rules
- `created`: notify the assignee.
- `updated`: use `wasChanged('assigned_to')` and `wasChanged('status')`. Only fire
  on reassignment and on the transition INTO the completed s
  timers/counters get written constantly — never notify on every update.
- Actor is `auth()->user()`; it is NULL in console/seeder co
  `?->` and fall back to a "System" label.

## Notifier rules
- Skip when actor === recipient. Nobody needs a WhatsApp for
- Phone normalization: strip non-digits, drop a leading `00`, prepend the country
  code when exactly 10 digits remain, reject anything under
  phones are ACTUALLY stored first (`select phone from users limit 10`) — they are
  usually free-text: "444 547 3439", "+1 (443) 665-4263".
- No usable phone → `Log::info` and return. Never throw.
- Missing API key → `Log::warning` and return. Never fail si
  env looks identical to a broken integration and wastes hours in production.
- Deep links must point at the recipient's own panel (admin
  they land on a 403.
- Message: bold title, then only the fields that are actuall

## Hard constraints
- NEVER modify migrations. If a migration blocks you, report it and stop.
- NEVER run `migrate:reset`, `migrate:fresh`, `migrate:refre
  destroy data, and seeders with fake phone numbers will send real WhatsApps to
  strangers.
- Do NOT create tests, artisan commands, or helper scripts unless asked.
- Do NOT fix unrelated bugs you find. Report them and move o
- Do NOT reformat files you touch; keep the diff to the lines you actually changed
  (watch out for the linter reformatting a whole file).

## Verify
Create a real record via tinker with two users that have phones, then confirm
delivery against the API itself, not just absence of errors:
`Sendify::messages(['limit' => 10, 'direction' => 'outgoing'])`
Timestamps must match record creation to the second (proves
`Sendify::Status()` never throws — use it to check the instance is `connected`;
`unlinked` means nobody has scanned the QR yet and nothing w

## Deployment note for the summary
`.env` is gitignored, so production needs the three vars added manually, then
`php artisan config:clear && php artisan config:cache`, then
Missing any of the three looks exactly like "the integration doesn't work".

```

On Laravel the service provider and the `Sendify` facade are auto-discovered. Publishing the configuration file is optional:

```
php artisan vendor:publish --tag=sendify-config
```

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

[](#configuration)

Add this to your application's `.env`:

```
SENDIFY_URL="https://sendify.mycompany.com"
SENDIFY_CLIENT="snd_live_xxxxxxxxxxxxxxxx"
SENDIFY_INSTANCE="sales"
```

VariableWhat it is`SENDIFY_URL`Base URL of that company's Sendify server (each company may run its own)`SENDIFY_CLIENT`API key of the instance; sent in the `X-API-Key` header`SENDIFY_INSTANCE`Numeric ID or name of the WhatsApp instanceThe underscore-less variants are also accepted: `SENDIFYURL`, `SENDIFYCLIENT`, `SENDIFYINSTANCE`.

Optional settings:

VariableDefaultWhat it does`SENDIFY_TIMEOUT``30`Request timeout, in seconds`SENDIFY_CONNECT_TIMEOUT``10`Connection timeout, in seconds`SENDIFY_RETRIES``1`Extra attempts for retryable `503` responses and for `429``SENDIFY_VERIFY_SSL``true`TLS certificate verification`SENDIFY_CONNECTION``default`Connection the facade uses when none is givenBesides the facade, the container also resolves the client by type hint:

```
use EverestHome\Sendify\Sendify;

public function __construct(private readonly Sendify $sendify)
{
}
```

Sending messages
----------------

[](#sending-messages)

PHP method names are case-insensitive, so `Sendify::TextMessageTo()` and `Sendify::textMessageTo()` are the exact same call. Pick whichever style you prefer.

```
use EverestHome\Sendify\Laravel\Facades\Sendify;

Sendify::TextMessageTo('+52 55 1234 5678', 'Your order is on its way');
Sendify::ImageMessageTo('5215551234567', 'https://cdn.mycompany.com/promo.jpg', 'Promo of the month');
Sendify::DocumentMessageTo('5215551234567', storage_path('app/invoices/F-1023.pdf'), 'F-1023.pdf', 'Your invoice');
Sendify::LocationMessageTo('5215551234567', 19.4326, -99.1332, 'Downtown branch');
Sendify::ContactMessageTo('5215551234567', 'Support', '5215557654321');
Sendify::PollMessageTo('5215551234567', 'Which time works for you?', ['Morning', 'Afternoon']);
Sendify::TemplateMessageTo('5215551234567', 'welcome', ['name' => 'Jovan']);
```

MethodNotes`textMessageTo(string $to, string $text)`Plain text`imageMessageTo(string $to, string $source, ?string $caption, ?string $mimetype)`See [Media](#media)`videoMessageTo(string $to, string $source, ?string $caption, ?string $mimetype)`See [Media](#media)`audioMessageTo(string $to, string $source, bool $ptt = false, ?string $mimetype)``$ptt = true` sends it as a voice note`documentMessageTo(string $to, string $source, ?string $filename, ?string $caption, ?string $mimetype)`Filename defaults to the local file's basename`stickerMessageTo(string $to, string $source, ?string $mimetype)`WebP stickers`locationMessageTo(string $to, float $lat, float $lng, ?string $description, ?string $address)`—`contactMessageTo(string $to, string $contactName, string $contactNumber)`Shares a vCard`pollMessageTo(string $to, string $name, array $options, ?int $selectableCount)`Between 2 and 12 options`templateMessageTo(string $to, string $template, array $variables = [])`Replaces the `{{placeholders}}` of a saved templatePhone numbers are normalized for you: `+52 55 1234 5678`, `5215551234567` and `5215551234567@c.us` all arrive the same. Group JIDs (`...@g.us`) and `@s.whatsapp.net` JIDs are passed through untouched.

Chained style when you send several things to the same chat:

```
Sendify::to('5215551234567')->text('Hi');
Sendify::to('5215551234567')->voiceNote(storage_path('app/audio/note.ogg'));
Sendify::to('5215551234567')->document(storage_path('app/invoices/F-1023.pdf'));
```

`to()` returns a `PendingMessage` exposing `text()`, `image()`, `video()`, `audio()`, `voiceNote()`, `document()`, `sticker()`, `location()`, `contact()`, `poll()`, `template()`, `reply()`, `forward()` and `messages()`.

### Media

[](#media)

Every media method accepts a public URL, a local path (it is read and sent as base64 with its mimetype), a `data:` URI, or raw base64 — with raw base64 you must pass the `mimetype` yourself.

Two service rules:

- **25 MB** maximum per base64 file (`Media::MAX_BYTES`). The client checks the size before uploading anything and throws `ValidationException` so you do not waste the round trip.
- **URLs are downloaded by the server**, so they must resolve to a public address: `localhost`, private LAN, link-local (including `169.254.169.254`) and CGNAT ranges are rejected with `400`. If your media server lives on the same private network as Sendify, the service has to run with `ALLOW_PRIVATE_MEDIA_URLS=true`.

```
Sendify::VideoMessageTo('5215551234567', public_path('videos/demo.mp4'), 'Demo');
Sendify::AudioMessageTo('5215551234567', 'https://cdn.mycompany.com/note.ogg', ptt: true);
Sendify::StickerMessageTo('5215551234567', 'https://cdn.mycompany.com/sticker.webp');
```

### Responses

[](#responses)

Every send returns a `Response` you can read as an array or through shortcuts:

```
$response = Sendify::TextMessageTo('5215551234567', 'Hi');

$response->messageId();      // 'BAE5...'
$response->successful();     // true
$response->status();         // 200
$response->data();           // the JSON `data` node
$response['data']['status']; // array-style access
$response->json('data.messageId');
$response->body();           // raw body — binary for messageMedia()
$response->header('retry-after');
```

### Bulk sending

[](#bulk-sending)

```
$batch = Sendify::BulkMessages([
    '5215551234567' => 'Hi Ana',
    '5215559876543' => 'Hi Luis',
]);

Sendify::batch($batch->json('id'));         // progress
Sendify::cancelBatch($batch->json('id'));
```

The long form is accepted too, in case two recipients share a number:

```
Sendify::BulkMessages([
    ['chatId' => '5215551234567', 'text' => 'Hi Ana'],
    ['chatId' => '5215559876543', 'text' => 'Hi Luis'],
]);
```

### Message actions

[](#message-actions)

```
Sendify::replyTo('5215551234567', $messageId, 'Of course');
Sendify::forwardTo('5215559876543', $messageId);
Sendify::react($messageId, '👍');
Sendify::editMessage($messageId, 'Fixed text');
Sendify::deleteMessage($messageId);
Sendify::pinMessage($messageId, 86400);   // 86400, 604800 or 2592000 seconds
Sendify::unpinMessage($messageId);
Sendify::starMessage($messageId);
Sendify::starMessage($messageId, false);  // unstar

Sendify::messages(['chatId' => '5215551234567', 'limit' => 50]);
Sendify::messageReactions($messageId);
Sendify::messageMedia($messageId)->body(); // raw attachment bytes
```

`messages()` accepts `chatId`, `direction`, `type`, `status`, `search`, `page` and `limit`.

Instance status
---------------

[](#instance-status)

`Sendify::Status()` is the full diagnosis and **never throws**: if the server is down, the instance does not exist, or the account is suspended, that is the status itself.

```
$status = Sendify::Status();

$status->state;            // EverestHome\Sendify\Enums\InstanceState::Suspended
$status->value();          // 'suspended'
$status->message;          // human-readable message (see note below)
$status->canSend();        // false
$status->accountProblem(); // true
$status->httpStatus;       // 401
```

`state`What happened`canSend()``connected`Connected to WhatsAppyes`connecting`Bringing the socket upno`qr_ready`A QR code is waiting to be scannedno`hibernated`Asleep to save RAM; sending wakes it up automaticallyyes`disconnected`Linked but not connectedno`unlinked`The phone was never linked or the session was closed: scan a QRno`instance_not_found`The instance does not exist or the API key does not own it (403/404)no`suspended`Instance deactivated or API key revoked (401)no`key_expired`API key expired: renewal or payment pending (401)no`missing_credentials`No API key was sent (401)no`ip_not_allowed`This server's IP is not in the key's allowlist (403)no`insufficient_role`The API key exists but its role is not enough (403)no`rate_limited`The per-minute request limit was hit (429)no`unreachable`The Sendify server did not answer: down, DNS, TLS or timeoutno`server_error`The Sendify server answered 5xxno```
use EverestHome\Sendify\Enums\InstanceState;

$status = Sendify::Status();

if ($status->canSend()) {
    Sendify::TextMessageTo($phone, $text);
} elseif ($status->accountProblem()) {
    // suspended, expired key, blocked IP or missing instance
    Notification::route('mail', 'admin@mycompany.com')->notify(new SendifyDown($status->message));
} elseif ($status->is(InstanceState::QrReady, InstanceState::Unlinked)) {
    // someone has to scan the QR: Sendify::qr()
}
```

Other shortcuts: `->connected()`, `->hibernated()`, `->suspended()`, `->needsAttention()`, `->needsStart()`, `->hasCredentials()`, `->hibernationReason()`, `->hibernatedAt()`, `->instanceId()`, `->instanceName()`, `->phone()`, `->pushName()`, `->business()`, `->lastConnectionAt()`, `->lastActiveAt()`, `->toArray()`. `Status` is also JSON-serializable, readable as an array (`$status['state']`) and castable to string (`"suspended: ..."`).

To follow the progress of a linking attempt: `->connecting()`, `->reconnecting()` (a retry is already scheduled — it is not idle), `->reconnectAttempts()`, `->qrAttempt()`, `->maxQrCycles()`and `->qrExpiresAt()`.

`hibernatedAt` and `hibernationReason` are only present while the instance is genuinely asleep. An instance that was never linked, or whose `logout()` wiped the session, reports `disconnected`without credentials, and the client translates that into `unlinked`.

To tell "server down" apart from "problem with this account" there is `Sendify::serverReachable()`(hits `/health`, no API key involved). `Sendify::healthLive()` is the liveness probe: it answers 200 as long as the process is alive even if the database is down — `health()` does check the database and returns 503 when it cannot reach it. And if you prefer the raw JSON with exceptions, use `Sendify::statusResponse()`.

> **Note:** `$status->message` and `InstanceState::label()` currently ship in Spanish, since they mirror the messages returned by the service. Use `$status->value()` or the `InstanceState` enum if you need a stable, language-independent value.

Instance lifecycle
------------------

[](#instance-lifecycle)

```
Sendify::connected();   // bool
Sendify::qr();          // { qr, qrExpiresAt, qrAttempt } — each QR lives 60 s
Sendify::start();       // opens the socket or emits a fresh QR
Sendify::stop();        // hibernates, keeping the session
Sendify::hibernate();
Sendify::wake();
Sendify::logout();      // unlinks the phone and DELETES the session
Sendify::forceKill();
Sendify::pairingCode('5215551234567');
Sendify::config();
Sendify::updateConfig(['idleTimeoutMs' => 900000, 'wakeTimeoutMs' => 8000]);
Sendify::stats();
```

`updateConfig()` accepts `hibernationEnabled`, `idleTimeoutMs`, `wakeTimeoutMs`, `maxQrCycles`, `syncFullHistory`, `active`, `autoReconnect`, `maxReconnectAttempts` and `reconnectDelayMs`.

API key roles: `read-only` reads status, history and stats; `operator` also sends, acts on messages and can call `wake()`; `admin` also manages the lifecycle, config, webhooks, templates, automations and profile. Reading the QR does not start anything: if the instance is asleep, `qr()` answers `qr: null` and `needsStart: true` until you call `start()`.

Group IDs (`...@g.us`) come from `GET /api/management/instances/:id/groups`, which uses a panel session instead of an API key, so that endpoint is not part of this client.

Webhooks, templates, automations, profile and statuses
------------------------------------------------------

[](#webhooks-templates-automations-profile-and-statuses)

```
$webhook = Sendify::webhooks()->create('CRM', 'https://crm.mycompany.com/sendify', [
    'message.received', 'message.sent', 'message.status', 'connection.updated',
]);

$secret = $webhook->json('secret'); // shown only once

Sendify::webhooks()->all();
Sendify::webhooks()->test($webhookId);
Sendify::webhooks()->update($webhookId, ['active' => false]);
Sendify::webhooks()->delete($webhookId);
Sendify::webhooks()->deliveries(['status' => 'failed']);

Sendify::templates()->create('welcome', 'Hi {{name}}, thanks for writing.');
Sendify::templates()->all();
Sendify::templates()->update($templateId, ['active' => false]);
Sendify::templates()->delete($templateId);

Sendify::automations()->create(
    name: 'Business hours',
    triggerType: 'message.received',
    conditions: ['contains' => 'hours'],
    actionType: 'send_text',
    actionPayload: ['text' => 'We are open from 9:00 to 18:00.'],
);

Sendify::profile()->name('Everest Home Support');
Sendify::profile()->status('Always online');       // the profile "about" text
Sendify::profile()->picture(public_path('logo.png'));
Sendify::profile()->removePicture();

Sendify::statuses()->text('We are online', backgroundColor: '#25D366');
Sendify::statuses()->media('image', public_path('promo.jpg'), 'This week only');
Sendify::statuses()->all();
Sendify::statuses()->delete($messageId);
```

Available events (`EverestHome\Sendify\Resources\Webhooks::EVENTS`): `message.received`, `message.sent`, `message.status`, `connection.updated`, `call.received`. Use `['*']` to receive everything. Failed deliveries are retried 5 times with exponential backoff (`2^attempts × 15 s`) and end up in `Sendify::webhooks()->deliveries()`.

To validate a delivery's signature in your Laravel controller:

```
use EverestHome\Sendify\Resources\Webhooks;

if (! Webhooks::verifySignature($request->getContent(), $request->header('X-Sendify-Signature', ''), config('services.sendify.secret'))) {
    abort(401);
}
```

Webhooks, automations and profile calls require an `admin` API key.

Multiple instances or servers
-----------------------------

[](#multiple-instances-or-servers)

Each company can run its own Sendify server. Add connections in `config/sendify.php`:

```
'connections' => [
    'default' => [
        'url' => env('SENDIFY_URL'),
        'client' => env('SENDIFY_CLIENT'),
        'instance' => env('SENDIFY_INSTANCE'),
    ],
    'billing' => [
        'url' => env('SENDIFY_BILLING_URL'),
        'client' => env('SENDIFY_BILLING_CLIENT'),
        'instance' => env('SENDIFY_BILLING_INSTANCE'),
    ],
],
```

```
Sendify::connection('billing')->TextMessageTo('5215551234567', 'Payment reminder');
```

Same API key, different instance:

```
Sendify::instance('support')->TextMessageTo('5215551234567', 'Hi');
```

Credentials that live in the database (multi-tenant):

```
use EverestHome\Sendify\SendifyManager;

$sendify = app(SendifyManager::class)->build([
    'url' => $tenant->sendify_url,
    'client' => $tenant->sendify_key,
    'instance' => $tenant->sendify_instance,
]);

$sendify->TextMessageTo($customer->phone, 'Hi');
```

`build()` inherits `timeout`, `connect_timeout`, `retries` and `verify_ssl` from `config/sendify.php` unless you override them in the array.

Error handling
--------------

[](#error-handling)

Any response outside the 2xx range throws an exception extending `SendifyException`:

ExceptionWhen`AuthenticationException`401/403: invalid or expired API key, IP not allowed`ValidationException`400/422: missing fields, invalid number, or media over 25 MB`NotFoundException`404: template, batch or message does not exist`InstanceNotConnectedException`409: the instance is not connected to WhatsApp`RateLimitException`429: the key's request limit was hit`InstanceAsleepException`503: hibernating, did not wake up in time (`retryAfter()`)`ConnectionException`No response at all: DNS, TLS, timeout`ConfigurationException`Missing credentials or an unknown connection nameRetryable 503 responses and 429s are retried automatically according to `SENDIFY_RETRIES`, waiting as long as the `Retry-After` header asks for (capped between 1 and 30 seconds).

On a 422 the service returns per-field errors; `$e->errors()` hands them back as-is:

```
[['field' => 'chatId', 'rule' => 'required', 'message' => 'chatId is required']]
```

```
use EverestHome\Sendify\Exceptions\InstanceAsleepException;
use EverestHome\Sendify\Exceptions\SendifyException;

try {
    Sendify::TextMessageTo($phone, $text);
} catch (InstanceAsleepException $e) {
    SendWhatsApp::dispatch($phone, $text)->delay(now()->addSeconds($e->retryAfter()));
} catch (SendifyException $e) {
    report($e);
}
```

Since everything throws, job retries on a Laravel queue come for free.

Using it without Laravel
------------------------

[](#using-it-without-laravel)

```
use EverestHome\Sendify\Sendify;

$sendify = Sendify::make('https://sendify.mycompany.com', 'snd_live_xxx', 'sales');

$sendify->textMessageTo('5215551234567', 'Hello from plain PHP');
```

`make()` takes an optional fourth argument with the same HTTP options as the config file:

```
$sendify = Sendify::make($url, $key, $instance, [
    'timeout' => 15,
    'retries' => 2,
    'verify_ssl' => false,
]);
```

To use another HTTP client (Guzzle, Laravel's, etc.) implement `EverestHome\Sendify\Http\ClientInterface` and pass it with `$sendify->withHttpClient($client)`, or bind it in the Laravel container.

Testing
-------

[](#testing)

```
composer install
composer test        # Pest
composer analyse     # PHPStan
composer format      # PHP-CS-Fixer
```

The test suite uses a fake HTTP client, so it never touches the network and does not need a running Sendify server. In your own tests, do the same: implement `ClientInterface` with canned responses and bind it in the container — the service provider resolves the HTTP client through that interface.

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance100

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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

5

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/adc677ac01d8160f79f53b68d627472f473b92fb1375c1d90c9608605cbd8307?d=identicon)[jovanlopez32](/maintainers/jovanlopez32)

---

Top Contributors

[![jovanlopez32](https://avatars.githubusercontent.com/u/49563725?v=4)](https://github.com/jovanlopez32 "jovanlopez32 (7 commits)")

---

Tags

laravelapi clientwhatsappeveresthomesendify

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/everesthome-sendify/health.svg)

```
[![Health](https://phpackages.com/badges/everesthome-sendify/health.svg)](https://phpackages.com/packages/everesthome-sendify)
```

###  Alternatives

[halilcosdu/laravel-replicate

Laravel client for Replicate predictions, files, models, trainings, and signed webhooks

5312.5k](/packages/halilcosdu-laravel-replicate)[blockshiftnetwork/chat-markdown-converter

Convert AI-generated Markdown to WhatsApp, Telegram, Discord, Slack and Instagram compatible formats using an Intermediate Representation (IR).

1213.5k](/packages/blockshiftnetwork-chat-markdown-converter)

PHPackages © 2026

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