Search by

alireza_malekei / peykad-sms

alirezaMalekei

Peykad SMS: a driver-based SMS sending package for Laravel with support for bulk and scheduled messages

Package info

github.com/alirezaMalekei/peykad-sms

pkg:composer/alireza_malekei/peykad-sms

Statistics

Installs: 27

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v3.0.3 2026-10-22 06:00 UTC

This package is auto-updated.

Last update: 2026-10-22 06:05:36 UTC


README

Send SMS in Laravel with support for multiple drivers. Send single, bulk and scheduled messages, and check your wallet with the same API key.

Contents

Compatibility

Laravel PHP
10.x 8.1 - 8.4
11.x 8.2+
12.x 8.2+
13.x 8.3+

Installation

In a fresh Laravel 13 application, use PHP 8.3 or newer and run the commands below. The same commands apply to the other supported Laravel versions.

composer require alireza_malekei/peykad-sms
php artisan vendor:publish --tag=sms-config

Laravel automatically discovers the package's service provider and facade aliases; no manual registration is needed.

Getting an API Key

Each developer must obtain their own API key and line number from their account panel at the service provider. This package does not ship with any default key.

The key is sent in the Authorization header of every request.

Setting the API Key

You can provide the key in one of three ways.

1. Environment file (recommended)

Add the key to your .env file:

SMS_DRIVER=panel
SMS_PANEL_URL=https://api.example.com
SMS_PANEL_ENDPOINT=/api/v1/send
SMS_PANEL_API_KEY=your-api-key
SMS_PANEL_LINE=1000002121

If your configuration is cached, clear it after changing .env:

php artisan config:clear

2. Config file

Publish the config file and edit config/sms.php in your project:

php artisan vendor:publish --tag=sms-config
'drivers' => [
    'panel' => [
        'api_key' => env('SMS_PANEL_API_KEY'),
    ],
],

Keep reading the key with env() instead of hard-coding it, because the config file is committed to Git.

3. At runtime

If your application uses multiple keys (for example, one per customer), pass the key when sending:

Sms::panel([
    'api_key'     => $customer->sms_api_key,
    'line_number' => $customer->sms_line,
])->send('09131234567', 'Hello');

Values passed at runtime take priority over .env and config values. Any option you omit falls back to the configured value.

Priority

  1. Values passed at runtime with Sms::panel([...])
  2. Values in your published config/sms.php
  3. Values in .env

If SMS_PANEL_API_KEY or SMS_PANEL_URL is missing, an SmsException is thrown before any request is sent.

Warning: Never hard-code the key or commit it to Git, and do not commit your .env file.

Usage

use AlirezaMalekei\PeykadSms\Facades\Sms;

// Simple send
Sms::send('09138332910', 'Hello');

// Bulk send + scheduling + campaign name + custom line number
Sms::send(
    ['09138332910', '09208332910'],
    'Hello',
    [
        'name'         => 'Test campaign',  // optional
        'line_number'  => '1000003030',     // optional, falls back to SMS_PANEL_LINE
        'scheduled_at' => now()->addHour(), // optional
    ]
);

// Custom line number (falls back to SMS_PANEL_LINE when omitted)
Sms::send('09138332910', 'Hello', ['line_number' => '1000003030']);

// Choose a driver
Sms::driver('panel')->send('09138332910', 'Hi');

The send Response

$result = Sms::send('09138332910', 'Hello');

$result->isSuccessful();  // true
$result->message();       // 'success'
$result->trackingCode();  // tracking code
$result->toArray();       // raw API response

If sending fails (an HTTP error, or success is false), an SmsException is thrown.

Getting the Wallet

Check how much credit your account has left. The wallet uses a separate WalletDriver, service, config block and facade.

Wallet requests always use GET with no payload and share SMS_PANEL_URL and SMS_PANEL_API_KEY with SMS sending. The only required wallet setting is the endpoint:

SMS_WALLET_ENDPOINT=/api/v1/wallet

Usage

use AlirezaMalekei\PeykadSms\Facades\Wallet;

$wallet = Wallet::get();

// Use a specific key, for example one per customer
$wallet = Wallet::get(['api_key' => $customer->sms_api_key]);

$wallet->balance();              // 50000, or null if the balance field is missing
$wallet->toArray();              // the wallet record
$wallet->get('panel.name');      // a single field (dot notation supported)
$wallet->raw();                  // raw API response (includes success, message, data)

An SmsException is thrown if the endpoint is not configured, the request fails or times out, the API returns success: false, or the wallet record is not found in the response.

If a class also imports your own Wallet model, alias one of them:

use AlirezaMalekei\PeykadSms\Facades\Wallet as SmsWallet;

Registering Users

Use Panel::register($data) to register a user through your panel API. Registration always sends a JSON POST request and shares SMS_PANEL_URL with SMS sending. Set USER_REGISTRATION_API_KEY to use a separate registration key; if it is not set, registration falls back to SMS_PANEL_API_KEY. The selected key is sent in the Authorization header.

Configuration

Set the registration endpoint and, optionally, a separate API key in your application's .env:

PANEL_REGISTRATION_ENDPOINT=/your-registration-endpoint
# Optional: omit this line to use SMS_PANEL_API_KEY
USER_REGISTRATION_API_KEY=your-registration-api-key

Replace the placeholder with your actual API route. Your published config/sms.php must include:

'registration' => [
    'endpoint' => env('PANEL_REGISTRATION_ENDPOINT'),
    'api_key' => env('USER_REGISTRATION_API_KEY', env('SMS_PANEL_API_KEY')),
],

If the config was published before registration was added, add this block to it. The registration key is optional; no HTTP method setting is needed. Key priority is: runtime api_key override, configured registration key, then the SMS panel key. The fallback reads the value of SMS_PANEL_API_KEY, rather than using its name as a literal key. If your configuration is cached, clear or rebuild it after updating these settings.

Usage

Pass a user data array to the facade. Validation stays in your project; the package forwards the data without local validation or phone normalization.

use AlirezaMalekei\PeykadSms\Facades\Panel;

// Values below come from data validated in your project.
// Include fields relevant to the selected user type; omit unused fields.
$result = Panel::register([
    'first_name' => $firstName,
    'last_name' => $lastName,
    'phone' => $phone,
    'email' => $email,
    'address' => $address,
    'national_id' => $nationalId,             // Required for individual users
    'type' => $type,                         // Your API's UserTypeEnum value
    'company_name' => $companyName,          // Required for legal users
    'company_phone' => $companyPhone,
    'company_email' => $companyEmail,
    'company_address' => $companyAddress,
    'company_national_id' => $companyNationalId, // Required for legal users
]);

Registration fields

The table below documents the API's validation rules. Your project validates the data before calling Panel::register(); this package only sends the supplied data.

Field Required Accepted value
first_name No String or null; may be omitted.
last_name No String or null; may be omitted.
phone Yes String accepted by ValidPhoneRule, using PhonePrefixEnum::fromNumber() for the prefix.
email No Valid email address or null; may be omitted.
address No String or null; may be omitted.
national_id For UserTypeEnum::Individual Valid Iranian national code (ir_national_code). Nullable when not required.
type Yes A valid backing value of your API's UserTypeEnum.
company_name For UserTypeEnum::Legal String when supplied.
company_phone No Mobile number accepted by ValidPhoneRule or landline accepted by ir_phone_area; may be omitted or null.
company_email No Valid email address or null; may be omitted.
company_address No String or null; may be omitted.
company_national_id For UserTypeEnum::Legal Valid Iranian company ID (ir_company_id) when supplied.

Use the actual Individual or Legal backing value defined by your API for type. These enums and custom validation rules belong to your application/API; the package does not provide or execute them. For optional fields without a nullable rule (company_name and company_national_id), omit unused fields rather than sending null.

You can also pass the validated data from your own Form Request:

$result = Panel::register($request->validated());

Registration response

Like Sms::send(), registration returns a response object. UserRegisterResponse provides:

$result->isSuccessful(); // API success flag (false if absent)
$result->message();      // API message, or null
$result->toArray();      // Complete decoded JSON response, including user data

The successful API response contains the registered user, developer credentials and selected panel under data. The example below uses placeholder personal data and credentials and shows one entry in user.panels; that array can contain multiple panels.

{
    "success": true,
    "message": "اطلاعات کاربر/شرکت ثبت شد",
    "data": {
        "user": {
            "id": 41,
            "first_name": "Example",
            "last_name": "User",
            "full_name": "Example User",
            "type": "individual",
            "national_id": 1234567890,
            "phone": 9120000000,
            "otp": 1234,
            "address": "Example address",
            "email": "user@example.test",
            "status": "verified",
            "reject_reason": null,
            "remember_token": null,
            "panels": [
                {
                    "id": 34,
                    "name": "Example Panel",
                    "code": 61605,
                    "admin_id": 41,
                    "is_main": 1,
                    "is_active": 1,
                    "is_trusted": 0,
                    "created_at": "2026-04-26T08:52:06.000000Z",
                    "admin_panel_name": "Example User / Example Panel / 61605",
                    "wallet": {
                        "id": 34,
                        "balance": 6000,
                        "panel_id": 34
                    },
                    "admin": {
                        "id": 41,
                        "first_name": "Example",
                        "last_name": "User",
                        "full_name": "Example User",
                        "type": "individual",
                        "national_id": 1234567890,
                        "phone": 9120000000,
                        "otp": 1234,
                        "address": "Example address",
                        "email": "user@example.test",
                        "status": "verified",
                        "reject_reason": null,
                        "remember_token": null
                    }
                }
            ],
            "legal_entity": null
        },
        "developer": {
            "id": 10,
            "api_name": "Example App",
            "api_usage": "send_sms",
            "api_key": "your-generated-developer-api-key",
            "user_id": 41,
            "panel_id": 34,
            "is_active": 1,
            "user": {
                "id": 41,
                "first_name": "Example",
                "last_name": "User",
                "full_name": "Example User",
                "type": "individual",
                "national_id": 1234567890,
                "phone": 9120000000,
                "otp": 1234,
                "address": "Example address",
                "email": "user@example.test",
                "status": "verified",
                "reject_reason": null,
                "remember_token": null
            }
        },
        "panel": {
            "id": 34,
            "name": "Example Panel",
            "code": 61605,
            "admin_id": 41,
            "is_main": 1,
            "is_active": 1,
            "is_trusted": 0,
            "created_at": "2026-04-26T08:52:06.000000Z",
            "admin_panel_name": "Example User / Example Panel / 61605",
            "wallet": {
                "id": 34,
                "balance": 6000,
                "panel_id": 34
            },
            "admin": {
                "id": 41,
                "first_name": "Example",
                "last_name": "User",
                "full_name": "Example User",
                "type": "individual",
                "national_id": 1234567890,
                "phone": 9120000000,
                "otp": 1234,
                "address": "Example address",
                "email": "user@example.test",
                "status": "verified",
                "reject_reason": null,
                "remember_token": null
            }
        }
    }
}
Response path Contents
success Whether registration succeeded.
message The API's registration message.
data.user Registered user details, associated panels and legal_entity (null in this individual-user example).
data.user.panels Panels associated with the user; each panel includes its wallet and admin details.
data.developer Developer record, including the generated api_key, user/panel IDs and nested user details.
data.panel Selected panel, including its wallet and admin.
data.panel.wallet.balance Balance of the selected panel's wallet.

Access these values through the response object's toArray() method:

$result = Panel::register($userData);

if ($result->isSuccessful()) {
    $response = $result->toArray();
    $message = $result->message();

    $user = $response['data']['user'];
    $developer = $response['data']['developer'];
    $panel = $response['data']['panel'];

    $userId = $user['id'];
    $panels = $user['panels'];
    $legalEntity = $user['legal_entity'];
    $apiKey = $developer['api_key'];
    $panelId = $panel['id'];
    $balance = $panel['wallet']['balance'];
}

The returned data.developer.api_key belongs to the developer record. It is separate from the configured key used to authorize the registration request; the package does not automatically replace your configured API keys.

Registration previously returned an array. Access the raw response through $result->toArray() when updating existing callers. The UserRegister::register() facade returns the same response object.

Runtime overrides

To override the registration endpoint, base URL or selected API key for one request:

$result = Panel::register($data, [
    'endpoint' => '/your-registration-endpoint',
    'api_key' => $customer->registration_api_key,
]);

Supported overrides are endpoint, base_url and api_key. Omitted values use the configured settings; requests always use POST.

Errors

When the registration API returns HTTP 422 with a JSON object or array, the package throws Laravel's HttpResponseException containing the original response body and status. Laravel returns the API's message and field errors as HTTP 422 to your caller. Let this exception reach Laravel's exception handler; a broad catch (\Throwable $e) that converts every exception to HTTP 500 would override this behavior.

Validation errors (HTTP 422)

For example, when phone and type are missing, the registration API returns HTTP 422 with this JSON body:

{
    "success": false,
    "message": "خطای اعتبارسنجی رخ داده‌است",
    "errors": {
        "phone": [
            "فیلد شماره تلفن همراه الزامی است"
        ],
        "type": [
            "فیلد نوع الزامی است"
        ]
    }
}

The package preserves success: false, the Persian message, and every field's message array under errors. The calling client can display the general message and the individual errors.phone and errors.type messages.

On this validation failure, Panel::register() throws HttpResponseException rather than returning a UserRegisterResponse. Laravel's exception handler sends the original JSON body with status 422; no local validation is added by this package.

Missing settings, connection failures, other HTTP errors, invalid JSON response shapes (including a non-JSON 422 response), and other API responses containing success: false throw SmsException:

use AlirezaMalekei\PeykadSms\Exceptions\SmsException;

try {
    $result = Panel::register($data);
} catch (SmsException $e) {
    report($e);
}

Adding a Custom Driver

Register your driver with extend:

Sms::extend('mygateway', fn () => new MyGatewayDriver());

Your class must implement AlirezaMalekei\PeykadSms\Contracts\SmsDriver:

use AlirezaMalekei\PeykadSms\Contracts\SmsDriver;
use AlirezaMalekei\PeykadSms\Responses\SmsResponse;

class MyGatewayDriver implements SmsDriver
{
    public function send(string|array $to, string $text, array $options = []): SmsResponse
    {
        // call your gateway here

        return new SmsResponse(true, 'sent', 'TRACKING-CODE');
    }
}

Custom drivers do not need anything for the wallet. Wallet::get() does not use drivers.

Error Handling

use AlirezaMalekei\PeykadSms\Exceptions\SmsException;
use Illuminate\Http\Client\ConnectionException;

try {
    Sms::send('09138332910', 'Hello');
} catch (SmsException $e) {
    // missing configuration, HTTP error, or success was false
    report($e);
} catch (ConnectionException $e) {
    // the panel could not be reached (DNS, timeout, refused connection)
    report($e);
}

Wallet::get() converts connection problems into SmsException, so it only needs the first catch.

If you send from a queued job, set a job timeout so a slow provider cannot block your workers.

Testing

composer install
composer test

In your own application, set SMS_DRIVER=log locally so no real messages are sent, or fake the HTTP calls:

Http::fake([
    '*' => Http::response([
        'success'       => true,
        'message'       => 'success',
        'tracking_code' => 'ABC123',
    ], 200),
]);

License

MIT