PHPackages                             awaisjameel/texto - 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. awaisjameel/texto

ActiveLibrary

awaisjameel/texto
=================

A Laravel package to handle messaging (SMS, MMS) using services Twilio or Telnyx

1.1.2(5mo ago)0430↓33.3%MITPHPPHP ^7.4 || ^8.1CI passing

Since Nov 6Pushed 5mo agoCompare

[ Source](https://github.com/awaisjameel/texto)[ Packagist](https://packagist.org/packages/awaisjameel/texto)[ Docs](https://github.com/awaisjameel/texto)[ GitHub Sponsors](https://github.com/sponsors/awaisjameel)[ RSS](/packages/awaisjameel-texto/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (14)Versions (12)Used By (0)

Texto
=====

[](#texto)

\*\* Unified, extensible Laravel gateway for sending &amp; receiving SMS/MMS over Twilio &amp; Telnyx. Batteries included: queueing, retries, events, webhooks, polling, typed value objects.\*\*

[![Latest Version on Packagist](https://camo.githubusercontent.com/8a7c1d40b1a384c2fcedc8fc43895e200838e0eb2d4b8b0d2e1301d64b544ad5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f61776169736a616d65656c2f746578746f2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/awaisjameel/texto)[![Tests](https://camo.githubusercontent.com/225a906383afd92e02bd3093dae204ebf416b307d7612ef1f274e5d5fa3f3af2/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f61776169736a616d65656c2f746578746f2f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/awaisjameel/texto/actions?query=workflow%3Atests+branch%3Amain)[![Downloads](https://camo.githubusercontent.com/a37d5295eff828e0fe8f2a9a8b1dceddd1d2920688ae27538784a0d26ae503fd/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f61776169736a616d65656c2f746578746f2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/awaisjameel/texto)[![License](https://camo.githubusercontent.com/422db9fd40f5831c765cf6530b6750c081b696bd18d904cf89554df98c676277/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e3f7374796c653d666c61742d737175617265)](LICENSE.md)

Texto provides a unified, extensible Laravel package for carrier-grade SMS/MMS messaging. Built for Laravel 10–12 (PHP 7.4 / 8.1+), it abstracts provider complexities (Twilio, Telnyx) through consistent contracts and value objects, enabling seamless integration with enterprise messaging workflows.

**Key Features:**

- **Unified API**: Single interface for sending SMS/MMS across multiple providers
- **Message Persistence**: Automatic storage of sent and received messages with full metadata
- **Status Tracking**: Real-time delivery status updates via webhooks and fallback polling
- **Event-Driven**: Rich event system for analytics, notifications, and custom automation
- **Advanced Twilio Support**: Conversations API with auto-provisioned content templates
- **Reliability**: Exponential backoff retry, queue-based async processing, and graceful degradation
- **Security**: Webhook signature validation, rate limiting, and shared secret protection
- **Extensibility**: Plugin architecture for adding new messaging providers

---

Table of Contents
-----------------

[](#table-of-contents)

1. Motivation &amp; Philosophy
2. Feature Overview
3. Quick Start
4. Installation
5. Configuration (`config/texto.php`)
6. Usage Examples
7. Queueing &amp; Async Flow
8. Events &amp; Observability
9. Data Model &amp; Persistence
10. Webhooks (Inbound + Status)
11. Security (Signatures, Secrets, Rate Limiting)
12. Status Polling (Adaptive Fallback)
13. Retry &amp; Backoff Strategy
14. Twilio Conversations &amp; Content Templates
15. Extending / Custom Drivers
16. Value Objects &amp; Enums
17. Console Commands
18. Testing, Fakes &amp; Local Development
19. Architecture Overview
20. Troubleshooting &amp; FAQ
21. Roadmap
22. Contributing
23. Security Policy
24. License &amp; Credits

---

1. Motivation &amp; Philosophy
------------------------------

[](#1-motivation--philosophy)

Building messaging features in Laravel applications often involves wrestling with provider-specific APIs that have inconsistent interfaces, error handling, and webhook formats. Without proper abstraction, code becomes littered with conditional logic that breaks when switching providers or adding new ones.

Texto solves this by providing a clean, consistent interface that:

- **Eliminates Provider Lock-in**: Switch between Twilio, Telnyx, or custom providers with minimal code changes
- **Ensures Type Safety**: Strongly typed enums and value objects prevent common mistakes
- **Promotes Clean Architecture**: Clear separation between sending, persistence, and status tracking
- **Enables Observability**: Comprehensive events and logging for monitoring and debugging
- **Handles Edge Cases**: Built-in retry logic, queueing, and fallback polling for reliability
- **Prioritizes Security**: Webhook validation, rate limiting, and shared secret protection

The philosophy is simple: messaging should be a first-class citizen in your Laravel app, not an afterthought that requires constant maintenance.

---

2. Feature Overview
-------------------

[](#2-feature-overview)

### Core Messaging

[](#core-messaging)

- **SMS &amp; MMS Support**: Send text messages and media attachments through Twilio and Telnyx
- **Unified API**: Single `Texto::send()` method works across all providers
- **Phone Number Validation**: Automatic E.164 formatting and validation using libphonenumber
- **Media Handling**: Support for multiple media URLs per message

### Reliability &amp; Performance

[](#reliability--performance)

- **Queue Integration**: Async message sending with Laravel queues for high-throughput applications
- **Retry Logic**: Exponential backoff for transient API failures
- **Status Polling**: Fallback polling when webhooks are delayed or unavailable
- **Rate Limiting**: Built-in protection against webhook abuse

### Advanced Twilio Features

[](#advanced-twilio-features)

- **Conversations API**: Rich conversation management with participant tracking
- **Content Templates**: Auto-provisioning and reuse of SMS/MMS templates
- **Template Variables**: Dynamic content insertion for personalized messaging

### Observability &amp; Events

[](#observability--events)

- **Event System**: Four key events (`MessageSent`, `MessageReceived`, `MessageFailed`, `MessageStatusUpdated`)
- **Structured Logging**: Comprehensive logging for debugging and monitoring
- **Metadata Capture**: Rich metadata storage including costs, segments, and custom data

### Security &amp; Compliance

[](#security--compliance)

- **Webhook Validation**: Signature verification for Twilio, shared secret headers
- **Rate Limiting**: Configurable per-minute limits on webhook endpoints
- **Data Persistence**: Optional message storage with configurable retention

### Developer Experience

[](#developer-experience)

- **Type Safety**: Strongly typed enums and value objects
- **Extensible Architecture**: Plugin system for custom providers
- **Testing Support**: Fake drivers and webhook validation skipping for tests
- **Laravel Integration**: Service provider auto-discovery and facade registration

---

3. Quick Start
--------------

[](#3-quick-start)

```
composer require awaisjameel/texto
php artisan texto:install   # publishes config + migration & runs migrate

php artisan texto:test-send +15551234567 "Hello from Texto"
```

```
use Texto; // facade alias configured automatically

Texto::send('+15551234567', 'Hello world');
```

---

4. Installation
---------------

[](#4-installation)

### Prerequisites

[](#prerequisites)

Before installing Texto, ensure your Laravel application meets these requirements:

- **Laravel**: 10.0, 11.0, or 12.0
- **PHP**: 7.4 or higher (8.2+ recommended)
- **Database**: MySQL, PostgreSQL, SQLite, or SQL Server
- **Queue System**: Any Laravel-supported queue driver (Database recommended for production)
- **PHP Extensions**: `ext-sodium` for Telnyx signature verification

### Quick Installation

[](#quick-installation)

The fastest way to get started:

```
composer require awaisjameel/texto
php artisan texto:install
```

This command will:

- Publish the configuration file to `config/texto.php`
- Publish and run the database migration
- Register the service provider and facade

### Manual Installation

[](#manual-installation)

For more control over the installation process:

```
# 1. Install the package
composer require awaisjameel/texto

# 2. Publish configuration (optional - auto-published by texto:install)
php artisan vendor:publish --tag=texto-config

# 3. Publish migration (optional - auto-published by texto:install)
php artisan vendor:publish --tag=texto-migrations

# 4. Run migrations
php artisan migrate
```

### Provider Setup

[](#provider-setup)

If you're not using package auto-discovery, add the service provider to `config/app.php`:

```
'providers' => [
    // ... other providers
    Awaisjameel\Texto\TextoServiceProvider::class,
],

'aliases' => [
    // ... other aliases
    'Texto' => Awaisjameel\Texto\Facades\Texto::class,
],
```

### Verification

[](#verification)

After installation, verify everything is working:

```
php artisan texto:test-send +15551234567 "Hello from Texto!"
```

This will send a test message using your configured provider and settings.

### Environment Variables

[](#environment-variables)

```
# Core
TEXTO_DRIVER=twilio                 # twilio | telnyx
TEXTO_STORE_MESSAGES=true           # disable to skip DB persistence
TEXTO_QUEUE=false                   # true => SendMessageJob async
TEXTO_RETRY_ATTEMPTS=3
TEXTO_RETRY_BACKOFF_START=200       # ms
TEXTO_WEBHOOK_SECRET=               # optional shared secret header
TEXTO_DEFAULT_REGION=US             # for parsing non-E.164 input

# Status polling (optional)
TEXTO_STATUS_POLL_ENABLED=false
TEXTO_STATUS_POLL_MIN_AGE=60
TEXTO_STATUS_POLL_MAX_ATTEMPTS=5
TEXTO_STATUS_POLL_QUEUED_MAX_ATTEMPTS=2
TEXTO_STATUS_POLL_BACKOFF=300
TEXTO_STATUS_POLL_BATCH=100

# Twilio
TWILIO_ACCOUNT_SID=...
TWILIO_AUTH_TOKEN=...
TWILIO_FROM_NUMBER=+15550001111
TWILIO_USE_CONVERSATIONS=true
TWILIO_SMS_TEMPLATE_FRIENDLY_NAME=texto_sms_template
TWILIO_MMS_TEMPLATE_FRIENDLY_NAME=texto_mms_template
TWILIO_CONVERSATION_PREFIX=Texto
TWILIO_CONVERSATION_WEBHOOK_URL=    # optional override

# Telnyx
TELNYX_API_KEY=...
TELNYX_MESSAGING_PROFILE_ID=...
TELNYX_FROM_NUMBER=+15550002222
TELNYX_WEBHOOK_SECRET=base64-encoded-public-key
TELNYX_HTTP_TIMEOUT=15              # seconds for outbound API calls
```

---

5. Configuration (`config/texto.php`)
-------------------------------------

[](#5-configuration-configtextophp)

After installation, you'll find the configuration file at `config/texto.php`. Here's a comprehensive guide to all available options:

### Core Settings

[](#core-settings)

KeyDefaultDescription`driver``'twilio'`Active messaging provider (`'twilio'` or `'telnyx'`)`store_messages``true`Whether to persist messages in the database`queue``false`Enable async message sending via Laravel queues`default_region``'US'`Default region for phone number parsing### Retry Configuration

[](#retry-configuration)

```
'retry' => [
    'max_attempts' => env('TEXTO_RETRY_ATTEMPTS', 3),
    'backoff_start_ms' => env('TEXTO_RETRY_BACKOFF_START', 200),
],
```

Controls exponential backoff retry behavior for failed API calls:

- `max_attempts`: Maximum number of retry attempts (default: 3)
- `backoff_start_ms`: Initial delay in milliseconds (doubles each retry)

### Webhook Security

[](#webhook-security)

```
'webhook' => [
    'secret' => env('TEXTO_WEBHOOK_SECRET'),
    'rate_limit' => env('TEXTO_WEBHOOK_RATE_LIMIT', 60),
],
```

- `secret`: Optional shared secret for webhook authentication
- `rate_limit`: Maximum webhook requests per minute (default: 60)

### Status Polling (Fallback)

[](#status-polling-fallback)

```
'status_polling' => [
    'enabled' => env('TEXTO_STATUS_POLL_ENABLED', false),
    'min_age_seconds' => env('TEXTO_STATUS_POLL_MIN_AGE', 60),
    'max_attempts' => env('TEXTO_STATUS_POLL_MAX_ATTEMPTS', 5),
    'queued_max_attempts' => env('TEXTO_STATUS_POLL_QUEUED_MAX_ATTEMPTS', 2),
    'backoff_seconds' => env('TEXTO_STATUS_POLL_BACKOFF', 300),
    'batch_limit' => env('TEXTO_STATUS_POLL_BATCH', 100),
],
```

Configures fallback polling for messages stuck in transient states:

- `enabled`: Enable/disable polling (default: false)
- `min_age_seconds`: Minimum age before polling starts
- `max_attempts`: Maximum polling attempts per message
- `backoff_seconds`: Delay between polling attempts

### Twilio Configuration

[](#twilio-configuration)

```
'twilio' => [
    'account_sid' => env('TWILIO_ACCOUNT_SID'),
    'auth_token' => env('TWILIO_AUTH_TOKEN'),
    'from_number' => env('TWILIO_FROM_NUMBER'),
    'use_conversations' => env('TWILIO_USE_CONVERSATIONS', true),
    'sms_template_friendly_name' => env('TWILIO_SMS_TEMPLATE_FRIENDLY_NAME', 'texto_sms_template'),
    'mms_template_friendly_name' => env('TWILIO_MMS_TEMPLATE_FRIENDLY_NAME', 'texto_mms_template'),
    'conversation_prefix' => env('TWILIO_CONVERSATION_PREFIX', 'Texto'),
    'conversation_webhook_url' => env('TWILIO_CONVERSATION_WEBHOOK_URL'),
],
```

Twilio-specific settings for both classic and Conversations API modes.

### Telnyx Configuration

[](#telnyx-configuration)

```
'telnyx' => [
    'api_key' => env('TELNYX_API_KEY'),
    'messaging_profile_id' => env('TELNYX_MESSAGING_PROFILE_ID'),
    'from_number' => env('TELNYX_FROM_NUMBER'),
    'webhook_secret' => env('TELNYX_WEBHOOK_SECRET'),
    'timeout' => env('TELNYX_HTTP_TIMEOUT', 15),
],
```

Telnyx API credentials, messaging profile configuration, the base64-encoded public key used to verify webhook signatures, and a transport timeout (seconds) for outbound REST calls.

### Testing Configuration

[](#testing-configuration)

```
'testing' => [
    'skip_webhook_validation' => env('TEXTO_TESTING_SKIP_WEBHOOK_VALIDATION', false),
],
```

Settings for testing environments to skip webhook signature validation.

---

6. Usage Examples
-----------------

[](#6-usage-examples)

### Basic SMS Sending

[](#basic-sms-sending)

Send a simple text message:

```
use Texto;

$result = Texto::send('+15551234567', 'Hello from Texto!');

// Returns SentMessageResult with status, provider ID, etc.
echo $result->status->value; // 'sent'
echo $result->providerMessageId; // 'SM1234567890abcdef'
```

### MMS with Media Attachments

[](#mms-with-media-attachments)

Send messages with images, videos, or other media:

```
$result = Texto::send('+15551234567', 'Check out this photo!', [
    'media_urls' => [
        'https://example.com/image.jpg',
        'https://example.com/video.mp4'
    ]
]);
```

### Per-Message Driver Override

[](#per-message-driver-override)

Temporarily use a different provider for specific messages:

```
// Send via Telnyx instead of default Twilio
$result = Texto::send('+15551234567', 'Via Telnyx', [
    'driver' => 'telnyx'
]);
```

### Custom Sender Number and Metadata

[](#custom-sender-number-and-metadata)

Use different sender numbers and attach custom metadata:

```
$result = Texto::send('+15551234567', 'Welcome to our service!', [
    'from' => '+15550009999', // Different sender number
    'metadata' => [
        'campaign' => 'welcome_series',
        'user_id' => 12345,
        'priority' => 'high'
    ]
]);
```

### Asynchronous Queue Processing

[](#asynchronous-queue-processing)

For high-throughput applications, enable queuing:

```
// In .env
TEXTO_QUEUE=true

// In code
$result = Texto::send('+15551234567', 'Queued message');
echo $result->status->value; // 'queued'

// Start a queue worker
php artisan queue:work
```

### Controller Response

[](#controller-response)

Return messages directly from controllers (auto-converts to JSON):

```
class NotificationController extends Controller
{
    public function sendAlert(Request $request)
    {
        $result = Texto::send(
            $request->phone,
            'Alert: ' . $request->message
        );

        // Automatically returns JSON response
        return $result;
    }
}
```

### Event-Driven Processing

[](#event-driven-processing)

Listen to messaging events for analytics and automation:

```
// In EventServiceProvider
protected $listen = [
    \Awaisjameel\Texto\Events\MessageSent::class => [
        \App\Listeners\LogMessageSent::class,
    ],
    \Awaisjameel\Texto\Events\MessageStatusUpdated::class => [
        \App\Listeners\TrackDeliveryStatus::class,
    ],
];

// Listener example
class TrackDeliveryStatus
{
    public function handle(MessageStatusUpdated $event)
    {
        $result = $event->result;

        // Log delivery metrics
        Log::info('Message delivered', [
            'provider_id' => $result->providerMessageId,
            'delivered_at' => now(),
        ]);
    }
}
```

### Bulk Messaging

[](#bulk-messaging)

Send multiple messages efficiently:

```
$recipients = ['+15551234567', '+15559876543', '+15551111111'];
$messages = [];

foreach ($recipients as $phone) {
    $messages[] = Texto::send($phone, 'Bulk notification');
}

// Process results
$successful = collect($messages)->where('status.value', 'sent')->count();
```

### International Number Handling

[](#international-number-handling)

Texto automatically handles international formatting:

```
// All of these work automatically
$phones = [
    '+1-555-123-4567',     // US format
    '555.123.4567',        // Local format (uses config region)
    '+44 20 7123 4567',    // UK format
    '0912345678',          // Indian format
];

foreach ($phones as $phone) {
    Texto::send($phone, 'International hello!');
}
```

---

7. Queueing &amp; Async Flow
----------------------------

[](#7-queueing--async-flow)

1. In queue mode, `Texto::send()` stores a queued row (status `queued`).
2. Dispatches `SendMessageJob` with deterministic primary key.
3. Job invokes `Texto::send(... ['queued_job'=>true,'queued_message_id'=>X])` to perform real API send.
4. Repository upgrades the exact queued record (no racey pattern matching).
5. Status webhooks or polling complete remaining transitions.

Benefits: immediate API responses, backpressure via Laravel queue, deterministic DB state.

> **Note:** The queued job now includes a snapshot of the active driver configuration (API keys, profile IDs, etc.) so workers and scheduled pollers have the same credentials that were present when the message was enqueued. Ensure your queue transport (e.g., database table, Redis) is appropriately protected since provider secrets travel with the job payload.

---

8. Events &amp; Observability
-----------------------------

[](#8-events--observability)

EventFired WhenPayload`MessageSent`Successful provider send`SentMessageResult``MessageFailed`Send attempt threw `TextoSendFailedException``SentMessageResult`, error message`MessageReceived`Inbound webhook parsed`WebhookProcessingResult``MessageStatusUpdated`Stored message status mutated (webhook)`WebhookProcessingResult`Subscribe in `EventServiceProvider` or use listeners/jobs for analytics, billing, triggers.

Structured logging is emitted at `info` / `debug` levels for sends, polling promotions, template initialization, and failures.

---

9. Data Model &amp; Persistence
-------------------------------

[](#9-data-model--persistence)

Table: `texto_messages`

ColumnNotesdirection`sent` / `received`driver`twilio` / `telnyx`from\_number / to\_numberE.164 formattedbodyNullable for pure media inboundmedia\_urlsJSON arraystatusNormalized enum (queued, sending, sent, delivered, failed, undelivered, received, ambiguous)provider\_message\_idSID / Telnyx ID (nullable until known)error\_codeProvider error (if any)segments\_count(Telnyx) part countcost\_estimate(Telnyx) estimated costmetadataArbitrary JSON (includes polling counters, conversation info)sent\_at / received\_at / status\_updated\_atTimestamps`Ambiguous` terminal state occurs when polling exhausts attempts without a provider id or final disposition.

---

10. Webhooks
------------

[](#10-webhooks)

Auto‑registered routes (POST):

PurposeTwilioTelnyxInbound`/texto/webhook/twilio``/texto/webhook/telnyx`Status`/texto/webhook/twilio` (same endpoint)`/texto/webhook/telnyx` (same endpoint)Both providers now publish inbound and status callbacks to a **single endpoint**. Texto inspects each payload to determine whether it is an inbound message or a delivery status update, ensuring identical processing for Twilio and Telnyx.

Each request passes through:

1. `VerifyTextoWebhookSecret` – matches `X-Texto-Secret` (if configured).
2. `RateLimitTextoWebhook` – per‑minute throttle (`webhook.rate_limit`).

Inbound payloads are normalized into `WebhookProcessingResult` then persisted via `EloquentMessageRepository`.

---

11. Security
------------

[](#11-security)

MechanismDescriptionTwilio SignatureValidated via `RequestValidator` unless `TEXTO_TESTING_SKIP_WEBHOOK_VALIDATION` in testing.Telnyx SignatureValidated via Ed25519 signature (Telnyx public webhook key, sodium required).Shared Secret HeaderAdd `TEXTO_WEBHOOK_SECRET` and send header `X-Texto-Secret`.Rate LimitingMiddleware prevents abuse of webhook endpoints.Phone ParsingAll numbers canonicalized using libphonenumber.---

12. Status Polling (Fallback)
-----------------------------

[](#12-status-polling-fallback)

Some production networks delay webhooks or they can be transiently disabled. Polling covers that gap.

Enable via `TEXTO_STATUS_POLL_ENABLED=true`. The service provider auto‑schedules `StatusPollJob` each minute. Logic:

- Select messages in transient states (`queued|sending|sent`) older than `min_age_seconds`.
- Skip if attempts exceed caps (`max_attempts`, or `queued_max_attempts` for still‑queued w/out provider id).
- Enforce backoff between polls via `last_poll_at` metadata.
- Promote forward‑only (e.g., queued -&gt; sent) while avoiding regressions.
- Mark terminal on delivered/failed/undelivered. Mark `ambiguous` when provider id missing after exhaustion.

Metadata counters (`poll_attempts`, `last_poll_at`, flags) are merged into `metadata` JSON for auditability.

---

13. Retry &amp; Backoff
-----------------------

[](#13-retry--backoff)

`Retry::exponential()` wraps critical provider API calls (send operations). Configured by `retry.max_attempts` &amp; `retry.backoff_start_ms`. Delay doubles each attempt until max attempts reached. Exceptions escalate as `TextoSendFailedException` leading to `MessageFailed` event emission and (optionally) DB record with status `failed`.

---

14. Twilio Conversations &amp; Content Templates
------------------------------------------------

[](#14-twilio-conversations--content-templates)

When `TWILIO_USE_CONVERSATIONS=true`, Texto:

1. Lazily initializes Conversations sub‑client.
2. Ensures (or creates) SMS / MMS Content Templates (friendly names configurable).
3. Creates (or reuses) a Conversation per send (deduplicates participant collisions &amp; reuses existing).
4. Optionally attaches per‑conversation webhook (config `conversation_webhook_url` or metadata override).
5. Sends message using template variables (splitting long body into up to 5 × 100‑char chunks). Falls back to body variant if template fails.

Captured metadata includes: `conversation_sid`, `conversation_reused`, optional `conversation_webhook_sid`.

Disable by setting `TWILIO_USE_CONVERSATIONS=false` to revert to classic Messages API.

#### Credential‑Aware Binding (New)

[](#credentialaware-binding-new)

As of 1.1.0 the package only binds Twilio (and Telnyx) low‑level API adapter singletons when their required credentials are present at boot time. This prevents accidental TypeErrors in test environments where env vars are intentionally omitted. If you rely on resolving (e.g.) `TwilioMessagingApiInterface` from the container in tests, ensure you either:

1. Provide fake credentials via env (e.g. `TWILIO_ACCOUNT_SID=AC_TEST`, `TWILIO_AUTH_TOKEN=test`), or
2. Manually bind a fake implementation in a test service provider.

The HTTP macro `Http::twilio()` is also credential‑aware; it omits Basic Auth when credentials are missing so generic tests can stub endpoints without failures.

#### Content Template Creation Robustness

[](#content-template-creation-robustness)

Template creation logic now tolerates varied (mock) response shapes and will parse a `sid` from either a direct field or nested content record arrays. No behavioral change is required for production usage; failures still fall back to body‑only send paths.

---

15. Extending / Custom Drivers
------------------------------

[](#15-extending--custom-drivers)

```
use Awaisjameel\Texto\Contracts\DriverManagerInterface;
use Awaisjameel\Texto\Contracts\MessageSenderInterface;
use Awaisjameel\Texto\ValueObjects\{PhoneNumber, SentMessageResult};
use Awaisjameel\Texto\Enums\{Driver, Direction, MessageStatus};

app(DriverManagerInterface::class)->extend('custom', function () {
    return new class implements MessageSenderInterface {
        public function send(PhoneNumber $to, string $body, ?PhoneNumber $from = null, array $mediaUrls = [], array $metadata = []): SentMessageResult {
            // ...call provider API...
            return new SentMessageResult(
                Driver::Twilio, // or introduce a new driver enum in a fork
                Direction::Sent,
                $to,
                $from,
                $body,
                $mediaUrls,
                $metadata,
                MessageStatus::Sent,
                'custom-123'
            );
        }
    };
});
```

Driver requirements:

- Implement `MessageSenderInterface::send()` returning `SentMessageResult`.
- Optionally expose `fetchStatus()` for polling compatibility.
- Throw `TextoSendFailedException` for terminal send failures.

---

API Reference
-------------

[](#api-reference)

### Texto Facade

[](#texto-facade)

The main entry point for all messaging operations.

#### `Texto::send(string $to, string $body, array $options = []): SentMessageResult`

[](#textosendstring-to-string-body-array-options---sentmessageresult)

Send an SMS or MMS message.

**Parameters:**

- `$to` (string): Recipient phone number (E.164 format or local format)
- `$body` (string): Message text content
- `$options` (array): Optional configuration

**Options:**

- `media_urls` (array): Array of media URLs for MMS
- `from` (string): Override sender number
- `driver` (string): Override provider ('twilio' or 'telnyx')
- `metadata` (array): Custom metadata to store with message
- `driver_config` (array): Optional provider configuration snapshot (API keys, messaging profile IDs, etc.) that temporarily overrides `config('texto.{driver}')` for this send; primarily used by queued jobs or multi-tenant flows.

> Note: When supplying `driver_config`, remember that any secrets included will travel with the queued job payload and logs you emit. Use encrypted queues or other safeguards appropriate for your environment.

**Returns:** `SentMessageResult` object

**Example:**

```
$result = Texto::send('+15551234567', 'Hello!', [
    'media_urls' => ['https://example.com/image.jpg'],
    'metadata' => ['campaign' => 'welcome']
]);
```

### Value Objects

[](#value-objects)

#### PhoneNumber

[](#phonenumber)

Represents a validated, E.164 formatted phone number.

```
class PhoneNumber
{
    public readonly string $e164;

    public static function fromString(string $raw, ?string $region = null): self
}
```

**Methods:**

- `fromString(string $raw, ?string $region = null)`: Parse and validate phone number
- `__toString()`: Returns E.164 formatted number

#### SentMessageResult

[](#sentmessageresult)

Immutable result object returned after sending a message.

```
final class SentMessageResult implements Responsable, JsonSerializable
{
    public readonly Driver $driver;
    public readonly Direction $direction;
    public readonly PhoneNumber $to;
    public readonly ?PhoneNumber $from;
    public readonly string $body;
    public readonly array $mediaUrls;
    public readonly array $metadata;
    public readonly MessageStatus $status;
    public readonly ?string $providerMessageId;
    public readonly ?string $errorCode;

    public function toArray(): array
    public function jsonSerialize(): array
    public function toResponse($request): JsonResponse
}
```

#### WebhookProcessingResult

[](#webhookprocessingresult)

Result object for webhook processing.

```
final class WebhookProcessingResult
{
    public readonly Driver $driver;
    public readonly Direction $direction;
    public readonly ?PhoneNumber $from;
    public readonly ?PhoneNumber $to;
    public readonly ?string $body;
    public readonly array $mediaUrls;
    public readonly array $metadata;
    public readonly ?string $providerMessageId;
    public readonly ?MessageStatus $status;

    public static function inbound(Driver $driver, PhoneNumber $from, PhoneNumber $to, ?string $body, array $media, array $metadata, ?string $providerMessageId = null): self
    public static function status(Driver $driver, ?string $providerMessageId, MessageStatus $status, array $metadata = []): self
}
```

### Enums

[](#enums)

#### MessageStatus

[](#messagestatus)

Normalized message status values.

```
enum MessageStatus: string
{
    case Queued = 'queued';      // Message queued for sending
    case Sending = 'sending';    // Message being sent
    case Sent = 'sent';          // Message sent successfully
    case Delivered = 'delivered'; // Message delivered to recipient
    case Received = 'received';  // Inbound message received
    case Failed = 'failed';      // Send failed permanently
    case Undelivered = 'undelivered'; // Message undelivered
    case Ambiguous = 'ambiguous'; // Status unknown after polling
}
```

#### Driver

[](#driver)

Available messaging providers.

```
enum Driver: string
{
    case Twilio = 'twilio';
    case Telnyx = 'telnyx';
}
```

#### Direction

[](#direction)

Message direction.

```
enum Direction: string
{
    case Sent = 'sent';
    case Received = 'received';
}
```

### Events

[](#events)

#### MessageSent

[](#messagesent)

Fired when a message is successfully sent.

```
class MessageSent
{
    public function __construct(public readonly SentMessageResult $result) {}
}
```

#### MessageReceived

[](#messagereceived)

Fired when an inbound message is received via webhook.

```
class MessageReceived
{
    public function __construct(public readonly WebhookProcessingResult $result) {}
}
```

#### MessageStatusUpdated

[](#messagestatusupdated)

Fired when a message status is updated via webhook or polling.

```
class MessageStatusUpdated
{
    public function __construct(public readonly WebhookProcessingResult $result) {}
}
```

#### MessageFailed

[](#messagefailed)

Fired when a message send attempt fails.

```
class MessageFailed
{
    public function __construct(
        public readonly SentMessageResult $result,
        public readonly ?string $reason = null
    ) {}
}
```

### Exceptions

[](#exceptions)

#### TextoException

[](#textoexception)

Base exception for all Texto-related errors.

#### TextoSendFailedException

[](#textosendfailedexception)

Thrown when message sending fails.

#### TextoWebhookValidationException

[](#textowebhookvalidationexception)

Thrown when webhook validation fails.

### Interfaces

[](#interfaces)

#### MessageSenderInterface

[](#messagesenderinterface)

Contract for message sending implementations.

```
interface MessageSenderInterface
{
    public function send(PhoneNumber $to, string $body, ?PhoneNumber $from = null, array $mediaUrls = [], array $metadata = []): SentMessageResult;
}
```

#### MessageRepositoryInterface

[](#messagerepositoryinterface)

Contract for message persistence.

```
interface MessageRepositoryInterface
{
    public function storeSent(SentMessageResult $result): Model;
    public function storeInbound(WebhookProcessingResult $result): Model;
    public function storeStatus(WebhookProcessingResult $result): ?Model;
    public function updatePolledStatus(Message $message, MessageStatus $status, array $extraMetadata = []): Message;
    public function upgradeQueued(int $id, SentMessageResult $result): ?Model;
}
```

#### DriverManagerInterface

[](#drivermanagerinterface)

Contract for driver management.

```
interface DriverManagerInterface
{
    public function sender(?Driver $driver = null): MessageSenderInterface;
    public function extend(string $name, callable $factory): void;
}
```

### Console Commands

[](#console-commands)

#### `php artisan texto:install`

[](#php-artisan-textoinstall)

Install and configure Texto.

#### `php artisan texto:test-send {to} {body?} {--driver=}`

[](#php-artisan-textotest-send-to-body---driver)

Send a test message.

**Parameters:**

- `to`: Recipient phone number
- `body`: Message body (default: "Test message")
- `--driver`: Override provider driver

---

17. Console Commands
--------------------

[](#17-console-commands)

CommandDescription`texto:install`Publish config + migration then run migrate.`texto:test-send {to} {body?}`Fire a manual test message (optional `--driver=`).`texto`Placeholder sample command.---

18. Testing, Fakes &amp; Local Development
------------------------------------------

[](#18-testing-fakes--local-development)

- Uses Pest &amp; Orchestra Testbench for package isolation.
- Static analysis via PHPStan (`composer analyse`).
- Code style via Pint (`composer format`).
- Swap drivers with a fake:

```
app(\Awaisjameel\Texto\Contracts\DriverManagerInterface::class)
    ->extend('twilio', fn () => new \Awaisjameel\Texto\Drivers\FakeSender());
```

- Skip webhook signature validation during tests: set `TEXTO_TESTING_SKIP_WEBHOOK_VALIDATION=true`.

Run full suite:

```
composer test
```

---

19. Architecture Overview
-------------------------

[](#19-architecture-overview)

LayerResponsibility`Texto` facade/rootOrchestrates send workflow, queue placeholder creation, events.`DriverManager`Resolves concrete sender implementation (built‑ins + extensions).Drivers (`TwilioSender`, `TelnyxSender`)Provider API invocation + provider‑specific metadata enrichment.`StatusMapper`Converts raw provider statuses / events to internal enum.`EloquentMessageRepository`Persistence &amp; deterministic queued upgrade + polling updates.Jobs (`SendMessageJob`, `StatusPollJob`)Async send &amp; periodic status reconciliation.Webhook HandlersParse &amp; validate inbound/status payloads per provider.Support Utilities (`Retry`, `PollingParameterResolver`, `TwilioContentService`)Cross‑cutting helpers.Value Objects / EnumsStrongly typed domain primitives.Design goals: minimal public API surface (`Texto::send`), encapsulated provider variance, explicit lifecycle events, observability via logs + metadata.

---

20. Troubleshooting &amp; FAQ
-----------------------------

[](#20-troubleshooting--faq)

### Common Issues

[](#common-issues)

**Q: Messages stuck in `queued` status**A: This usually indicates queue processing issues.

- Verify `TEXTO_QUEUE=true` in your environment
- Ensure a queue worker is running: `php artisan queue:work`
- Check queue connection configuration
- Review Laravel logs for job processing errors
- Enable status polling as fallback: `TEXTO_STATUS_POLL_ENABLED=true`

**Q: Webhook signature validation fails (401 errors)**A: Signature validation ensures webhook authenticity.

- For Twilio: Verify `TWILIO_AUTH_TOKEN` matches your Twilio console
- Ensure webhook URLs in provider console exactly match your routes (including protocol)
- For local development, use ngrok or similar tunneling service
- Check that webhook URLs don't have trailing slashes or query parameters

**Q: Twilio Conversations template creation warnings**A: Template auto-provisioning may fail due to permissions.

- This is non-fatal; Texto falls back to direct message sending
- Check Twilio account has Content API permissions
- Verify `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` are correct
- Template creation warnings don't prevent message sending

**Q: Telnyx cost/segment data missing**A: Cost and segment information is only provided in specific response scenarios.

- Ensure your Telnyx API key has messaging permissions
- Cost data appears only when Telnyx includes it in API responses
- Segment counts depend on message content and provider logic

**Q: Messages failing with provider errors**A: Check provider account status and configuration.

- Verify API credentials are correct and active
- Ensure sender numbers are verified/purchased in provider console
- Check provider account has sufficient balance/credits
- Review message content for prohibited terms

**Q: High memory usage with large message volumes**A: Optimize for high-throughput scenarios.

- Enable queuing: `TEXTO_QUEUE=true`
- Use database queue driver for reliability
- Configure appropriate queue worker settings
- Monitor queue depth and processing rates

### Status Definitions

[](#status-definitions)

**Q: What does `ambiguous` status mean?**A: Messages reach ambiguous status when polling exhausts all attempts without determining final delivery status.

- Occurs when provider ID is missing and polling can't retrieve status
- Investigate upstream provider logs for root cause
- May indicate provider API issues or message filtering

**Q: Difference between `failed` and `undelivered`?**A: These represent different failure modes:

- `failed`: Immediate sending failure (invalid number, blocked content, etc.)
- `undelivered`: Message sent but delivery failed (phone off, full mailbox, etc.)

### Configuration Issues

[](#configuration-issues)

**Q: How to disable message persistence?**A: Set `TEXTO_STORE_MESSAGES=false` in your environment.

- Events will still fire normally
- `SentMessageResult` objects are still returned
- Useful for testing or when external logging is preferred

**Q: Phone number validation too strict**A: Adjust the default region for number parsing.

- Set `TEXTO_DEFAULT_REGION` to your primary market (e.g., 'GB' for UK)
- This affects how local format numbers are interpreted
- E.164 format (+country code) always works regardless of region

### Provider-Specific Issues

[](#provider-specific-issues)

**Q: Twilio rate limiting**A: Twilio enforces sending limits based on account type.

- Free accounts: 100 messages/day
- Trial accounts: Limited sending
- Full accounts: Higher limits based on verification level
- Implement queuing and backoff strategies

**Q: Telnyx webhook delays**A: Telnyx webhooks may have higher latency than Twilio.

- Enable status polling for critical delivery tracking
- Configure appropriate polling intervals
- Monitor webhook delivery logs

### Performance Tuning

[](#performance-tuning)

**Q: Optimizing for high volume**A: Several configuration options for performance:

- Use Redis/database queues instead of sync processing
- Configure multiple queue workers
- Enable status polling with appropriate batch sizes
- Monitor database indexes on `texto_messages` table
- Consider message archiving for old records

**Q: Database performance with many messages**A: The `texto_messages` table can grow quickly.

- Add database indexes on frequently queried columns
- Implement message archiving/cleanup strategies
- Consider partitioning for very high volume
- Monitor query performance and optimize as needed

### Development &amp; Testing

[](#development--testing)

**Q: Testing without sending real messages**A: Use the fake driver for testing:

```
app(DriverManagerInterface::class)->extend('twilio', fn() => new FakeSender());
```

- Skip webhook validation in tests: `TEXTO_TESTING_SKIP_WEBHOOK_VALIDATION=true`
- Use test credentials or mock HTTP responses

**Q: Local development with webhooks**A: Webhooks require public URLs for provider access.

- Use ngrok, localtunnel, or similar services
- Configure webhook URLs in provider console
- Consider webhook testing tools like webhook.site for debugging

### Extending Texto

[](#extending-texto)

**Q: Adding a new provider (e.g., Vonage)**A: Implement the extension pattern:

```
app(DriverManagerInterface::class)->extend('vonage', function() {
    return new class implements MessageSenderInterface {
        public function send(PhoneNumber $to, string $body, ?PhoneNumber $from = null, array $mediaUrls = [], array $metadata = []): SentMessageResult {
            // Your implementation
        }
    };
});
```

- Consider contributing back via PR for official support
- Follow existing driver patterns for consistency

**Q: Custom webhook handling**A: Extend webhook handlers for custom logic:

- Create custom handler class implementing `WebhookHandlerInterface`
- Register in service provider or route configuration
- Handle provider-specific webhook formats

---

21. Performance Considerations &amp; Best Practices
---------------------------------------------------

[](#21-performance-considerations--best-practices)

### Database Optimization

[](#database-optimization)

For high-volume applications, optimize the `texto_messages` table:

```
-- Add performance indexes
CREATE INDEX idx_texto_messages_status_created ON texto_messages (status, created_at);
CREATE INDEX idx_texto_messages_provider_id ON texto_messages (provider_message_id);
CREATE INDEX idx_texto_messages_from_to ON texto_messages (from_number, to_number);

-- Consider partitioning for very high volume
-- Partition by month for message archiving
ALTER TABLE texto_messages PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026)
);
```

### Queue Configuration

[](#queue-configuration)

For reliable message processing at scale:

```
// config/queue.php
'connections' => [
    'database' => [
        'driver' => 'database',
        'table' => 'jobs',
        'queue' => 'default',
        'retry_after' => 90, // Increase for messaging jobs
    ],
],
```

Run multiple workers for high throughput:

```
# Multiple workers for parallel processing
php artisan queue:work --queue=texto-high,texto-normal --max-jobs=1000 --sleep=3
php artisan queue:work --queue=texto-bulk --max-jobs=500 --sleep=5
```

### Monitoring &amp; Alerting

[](#monitoring--alerting)

Implement monitoring for critical messaging operations:

```
// Monitor queue health
$pendingJobs = DB::table('jobs')->where('queue', 'like', 'texto%')->count();
if ($pendingJobs > 1000) {
    Log::warning('High texto queue backlog', ['count' => $pendingJobs]);
}

// Monitor failure rates
$failureRate = Message::where('status', 'failed')
    ->where('created_at', '>', now()->subHour())
    ->count() / Message::where('created_at', '>', now()->subHour())->count();

if ($failureRate > 0.1) { // 10% failure rate
    // Alert or take action
}
```

### Cost Optimization

[](#cost-optimization)

Track and optimize messaging costs:

```
// Analyze costs by provider and campaign
$costs = Message::selectRaw('
        driver,
        SUM(cost_estimate) as total_cost,
        COUNT(*) as message_count,
        AVG(cost_estimate) as avg_cost
    ')
    ->whereNotNull('cost_estimate')
    ->where('created_at', '>', now()->subMonth())
    ->groupBy('driver')
    ->get();

// Implement cost thresholds
if ($costs->sum('total_cost') > 1000) { // Monthly budget
    // Send alert or implement throttling
}
```

### Security Best Practices

[](#security-best-practices)

Secure your messaging infrastructure:

```
// Use environment variables for secrets
// Never commit API keys to version control

// Implement rate limiting per user/phone
RateLimiter::for('texto-send', function (Request $request) {
    return Limit::perMinute(10)->by($request->user()->id);
});

// Validate phone numbers strictly
$phone = PhoneNumber::fromString($request->phone, 'US'); // Specify region
if (!$phone) {
    throw new InvalidPhoneNumberException();
}
```

### Error Handling &amp; Resilience

[](#error-handling--resilience)

Implement comprehensive error handling:

```
try {
    $result = Texto::send($phone, $message, $options);
} catch (TextoSendFailedException $e) {
    // Log detailed error
    Log::error('Message send failed', [
        'phone' => $phone,
        'error' => $e->getMessage(),
        'driver' => config('texto.driver')
    ]);

    // Implement fallback logic
    if (config('texto.driver') === 'twilio') {
        // Try Telnyx as fallback
        $result = Texto::send($phone, $message, ['driver' => 'telnyx'] + $options);
    }

    // Notify user or take alternative action
}
```

### Testing Strategies

[](#testing-strategies)

Comprehensive testing approach:

```
// Unit tests for drivers
class TwilioSenderTest extends TestCase
{
    public function test_sends_message_successfully()
    {
        // Mock Twilio client
        $this->mock(TwilioClient::class, function ($mock) {
            $mock->shouldReceive('messages->create')
                ->once()
                ->andReturn((object)['sid' => 'SM123']);
        });

        $result = app(TwilioSender::class)->send(
            PhoneNumber::fromString('+15551234567'),
            'Test message'
        );

        $this->assertEquals(MessageStatus::Sent, $result->status);
    }
}

// Integration tests with fake driver
class MessagingIntegrationTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        // Use fake driver for integration tests
        app(DriverManagerInterface::class)->extend('twilio', fn() => new FakeSender());
    }
}
```

### Scaling Considerations

[](#scaling-considerations)

For enterprise-level messaging:

1. **Horizontal Scaling**: Deploy across multiple servers with shared queue
2. **Database Sharding**: Split message storage across multiple databases
3. **CDN for Media**: Use CDNs for MMS media to reduce bandwidth
4. **Provider Redundancy**: Implement multi-provider failover logic
5. **Caching**: Cache frequently used phone number validations
6. **Async Processing**: Always use queues for production deployments

### Compliance &amp; Data Protection

[](#compliance--data-protection)

Handle sensitive messaging data appropriately:

```
// Implement data retention policies
Message::where('created_at', ' env('TEXTO_DRIVER', 'twilio'),

// New (1.x) - same, but additional options available
'driver' => env('TEXTO_DRIVER', 'twilio'),
'store_messages' => env('TEXTO_STORE_MESSAGES', true),
'queue' => env('TEXTO_QUEUE', false),
```

**API Changes:**

```
// Old (0.x)
Texto::send('+15551234567', 'Hello');

// New (1.x) - same API, enhanced return type
$result = Texto::send('+15551234567', 'Hello');
$result->status; // Now returns MessageStatus enum
```

**Migration Steps:**

1. Backup your database
2. Update to 1.x: `composer update awaisjameel/texto`
3. Run `php artisan texto:install` to update config and migrations
4. Update any code using status strings to use `MessageStatus` enums
5. Test thoroughly in staging environment

### Environment Variable Changes

[](#environment-variable-changes)

Old VariableNew VariableNotes-`TEXTO_STORE_MESSAGES`Control message persistence-`TEXTO_QUEUE`Enable async processing-`TEXTO_WEBHOOK_SECRET`Shared secret for webhook auth-`TEXTO_STATUS_POLL_ENABLED`Enable status polling fallback### Database Schema Changes

[](#database-schema-changes)

Version 1.x adds new columns to `texto_messages`:

```
-- New columns in 1.x
ALTER TABLE texto_messages ADD COLUMN segments_count INT NULL;
ALTER TABLE texto_messages ADD COLUMN cost_estimate DECIMAL(10,4) NULL;
ALTER TABLE texto_messages ADD COLUMN status_updated_at TIMESTAMP NULL;
```

These are nullable and backward compatible.

### Webhook URL Changes

[](#webhook-url-changes)

Webhook routes remain the same but include enhanced validation:

- `/texto/webhook/twilio` - Twilio webhooks (inbound + status)
- `/texto/webhook/telnyx` - Telnyx webhooks (inbound + status)

Ensure your provider console webhook URLs match exactly.

### Testing Changes

[](#testing-changes)

Update your tests to use the new `FakeSender`:

```
// Old approach
// Custom mock setup

// New approach
app(DriverManagerInterface::class)->extend('twilio', fn() => new FakeSender());
```

25. License &amp; Credits
-------------------------

[](#25-license--credits)

### License

[](#license)

Released under the MIT License. See [LICENSE.md](LICENSE.md) for details.

### Credits

[](#credits)

**Created by:** [awaisjameel](https://github.com/awaisjameel)

**Inspiration &amp; Thanks:**

- [Spatie Laravel Package Tools](https://github.com/spatie/laravel-package-tools) - Package skeleton
- Laravel OSS Ecosystem - Best practices and patterns
- Twilio &amp; Telnyx Developer Communities - API insights

### Contributors

[](#contributors)

We'd like to thank all contributors who have helped make Texto better:

- [awaisjameel](https://github.com/awaisjameel)

### Sponsors

[](#sponsors)

Support Texto's development:

[![GitHub Sponsors](https://camo.githubusercontent.com/97a404242afeca2eba0be9f16446c4f7631b3012aaf3c34373d71566cd1c5557/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73706f6e736f72732f61776169736a616d65656c)](https://github.com/sponsors/awaisjameel)

### Related Projects

[](#related-projects)

- [Laravel Notification Channels](https://github.com/laravel-notification-channels) - Alternative notification approach
- [Twilio PHP SDK](https://github.com/twilio/twilio-php) - Official Twilio library
- [Telnyx Messaging API Reference](https://developers.telnyx.com/docs/api/v2/messaging/Messages) - REST endpoints used by Texto's Telnyx driver

---

*Made with ❤️ for the Laravel community*

---

### Quick Reference Cheat Sheet

[](#quick-reference-cheat-sheet)

```
// Basic SMS
$result = Texto::send('+15551234567', 'Hello World!');

// MMS with media
$result = Texto::send('+15551234567', 'Check this out!', [
    'media_urls' => ['https://example.com/image.jpg']
]);

// Override provider per message
$result = Texto::send('+15551234567', 'Via Telnyx', [
    'driver' => 'telnyx'
]);

// Custom sender and metadata
$result = Texto::send('+15551234567', 'Promotional message', [
    'from' => '+15550009999',
    'metadata' => ['campaign' => 'spring_sale', 'priority' => 'high']
]);

// Async processing (when TEXTO_QUEUE=true)
$result = Texto::send('+15551234567', 'Queued message');
// $result->status === MessageStatus::Queued

// Event listeners
Event::listen(MessageSent::class, function ($event) {
    Log::info('Message sent', ['id' => $event->result->providerMessageId]);
});
```

---

Support &amp; Community
-----------------------

[](#support--community)

- 📖 **Documentation**: You're reading it! Check the [GitHub repository](https://github.com/awaisjameel/texto) for the latest updates
- 🐛 **Bug Reports**: [Open an issue](https://github.com/awaisjameel/texto/issues) on GitHub
- 💡 **Feature Requests**: [Start a discussion](https://github.com/awaisjameel/texto/discussions) on GitHub
- 💬 **Community Chat**: Join our [Discord server](https://discord.gg/texto) for real-time help
- ⭐ **Show Support**: Star the repo if Texto saves you time and effort!

---

*Built with ❤️ for the Laravel community by [awaisjameel](https://github.com/awaisjameel)*

###  Health Score

39

—

LowBetter than 86% of packages

Maintenance70

Regular maintenance activity

Popularity16

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 97.1% 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 ~2 days

Total

11

Last Release

167d ago

PHP version history (3 changes)v1.0.0PHP &gt;=7.4 || &gt;=8.2

1.0.2PHP ^7.4 || ^8.2

1.0.4PHP ^7.4 || ^8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/8478f5cd00831255bda5f4ab259a8761a8001142756ab90bf8804388a78b2036?d=identicon)[awaisjameel](/maintainers/awaisjameel)

---

Top Contributors

[![awaisjameel](https://avatars.githubusercontent.com/u/9046343?v=4)](https://github.com/awaisjameel "awaisjameel (33 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

laravelsmstwiliomessagingmessagesAwaisJameelmmstelnyxtexto

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/awaisjameel-texto/health.svg)

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

###  Alternatives

[laravel-notification-channels/twilio

Provides Twilio notification channel for Laravel

2587.7M12](/packages/laravel-notification-channels-twilio)[clickbar/laravel-magellan

This package provides functionality for working with the postgis extension in Laravel.

423715.4k1](/packages/clickbar-laravel-magellan)[spatie/laravel-prometheus

Export Laravel metrics to Prometheus

2651.3M6](/packages/spatie-laravel-prometheus)[tzsk/sms

A robust and unified SMS gateway integration package for Laravel, supporting multiple providers.

320244.3k6](/packages/tzsk-sms)[vormkracht10/laravel-mails

Laravel Mails can collect everything you might want to track about the mails that has been sent by your Laravel app.

24149.7k](/packages/vormkracht10-laravel-mails)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

3786.5k](/packages/harris21-laravel-fuse)

PHPackages © 2026

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