siberfx / netgsm
NetGsm SMS, OTP, reporting, balance and IYS integration for Laravel
Requires
- php: ^8.4
- guzzlehttp/guzzle: ^7.9
- illuminate/notifications: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- illuminate/translation: ^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.24
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.5|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
SMS, OTP, delivery reports, balance and İYS consent management for Laravel,
built on the NetGsm REST v2 API.
Features
- Notification channel: send SMS from any Laravel notification with
NetGsmChannel. - SMS and OTP: one recipient or many, a different text per recipient, scheduling, Turkish characters and İYS filters.
- Job management: cancel scheduled messages and list your sender names.
- Reports: delivery reports with automatic pagination, plus per-job statistics.
- Balance: remaining credit (TL) and package balances.
- İYS: add and search consent records, single or in bulk.
- Safe defaults: credentials go over HTTP Basic auth, and NetGsm error codes come back as translated exceptions (English and Turkish).
Requirements
| Package | PHP | Laravel |
|---|---|---|
^5.0 |
8.4, 8.5 | 12.x, 13.x |
You also need a NetGsm account with an API user (created in the NetGsm panel) and at least one approved sender name (msgheader).
Contents
- Quick start
- Configuration
- Sending SMS
- Reports
- Account balance
- İYS integration
- Error handling
- Testing your application
- API reference
- Troubleshooting
- Upgrading from 4.x
- Contributing
- Security
- Credits
- License
Quick start
1. Install the package
composer require siberfx/netgsm
The service provider and the NetGsm facade alias are registered automatically.
2. Add your credentials to .env
NETGSM_USERCODE=8508xxxxxx NETGSM_SECRET=your-api-password NETGSM_HEADER=COMPANY
3. Send a message
use Siberfx\NetGsm\NetGsmFacade as NetGsm; use Siberfx\NetGsm\Sms\NetGsmSmsMessage; $jobId = NetGsm::sendSms( (new NetGsmSmsMessage('Hello from Laravel!'))->setRecipients('5051234567') );
Configuration
All options can be set from .env. To change the defaults in code, publish the config file:
php artisan vendor:publish --tag=netgsm-config # config/netgsm.php php artisan vendor:publish --tag=netgsm-lang # lang/vendor/netgsm
| Variable | Required | Default | Description |
|---|---|---|---|
NETGSM_USERCODE |
Yes | – | API username (usually your subscriber number, e.g. 8508xxxxxx). |
NETGSM_SECRET |
Yes | – | API password. |
NETGSM_HEADER |
Yes | – | Default sender name, used when a message doesn't set one. |
NETGSM_BRANDCODE |
For İYS | – | İYS brand code. |
NETGSM_ENCODING |
No | – | Set to TR if your messages contain Turkish characters. |
NETGSM_APPNAME |
No | – | Application name reported to NetGsm with each request. |
NETGSM_PARTNER_CODE |
No | – | NetGsm partner code. |
NETGSM_BASE_URI |
No | https://api.netgsm.com.tr |
API base URL. |
NETGSM_TIMEOUT |
No | 60 |
HTTP timeout in seconds. |
Sending SMS
With notifications
Tell your notifiable model where to send messages:
public function routeNotificationForNetGsm(): string { // One number, or several separated by commas: "5051234567, 5441234568" return $this->phone; }
Add NetGsmChannel to via() and build the message in toNetGsm():
use Illuminate\Notifications\Notification; use Siberfx\NetGsm\NetGsmChannel; use Siberfx\NetGsm\Sms\NetGsmSmsMessage; class OrderShipped extends Notification { public function __construct(private Order $order) {} public function via(object $notifiable): array { return [NetGsmChannel::class]; } public function toNetGsm(object $notifiable): NetGsmSmsMessage { return new NetGsmSmsMessage("Your order #{$this->order->id} has shipped!"); } }
$user->notify(new OrderShipped($order));
Recipients set on the message take precedence over the model's route:
return (new NetGsmSmsMessage('Your order has shipped!'))->setRecipients(['5051234567', '5441234568']);
The channel works with queued notifications (ShouldQueue) like any other Laravel channel.
With the facade
use Siberfx\NetGsm\NetGsmFacade as NetGsm; use Siberfx\NetGsm\Sms\NetGsmSmsMessage; $message = (new NetGsmSmsMessage('Your order has shipped!')) ->setHeader('COMPANY') ->setRecipients(['5051234567', '5441234568']); $jobId = NetGsm::sendSms($message); // "17377215342605050417149344"
Keep the returned job id to query reports or cancel the message later. You can also inject
Siberfx\NetGsm\NetGsm instead of using the facade.
A different text for each recipient
Use addMessage() to send personalised texts in a single request. Recipients without their own text get the
default message.
$message = (new NetGsmSmsMessage('Your appointment is tomorrow.')) ->setRecipients('5051234567') // gets the default text ->addMessage('5441234568', 'Ayşe, your appointment is at 10:00.') ->addMessage('5321234567', 'Mehmet, your appointment is at 14:30.'); NetGsm::sendSms($message);
Scheduling, encoding and İYS
use Siberfx\NetGsm\Sms\NetGsmSmsMessage; (new NetGsmSmsMessage('Great note from the future!')) ->setHeader('COMPANY') // sender name, defaults to NETGSM_HEADER ->setStartDate(now()->addDays(10)) // send later ->setEndDate(now()->addDays(11)) // stop retrying after this date ->setEncoding('TR') // Turkish characters (ç, ğ, ı, ö, ş, ü) ->setIysFilter(NetGsmSmsMessage::IYS_FILTER_COMMERCIAL_INDIVIDUAL) ->setPartnerCode('PARTNER');
| İYS filter constant | Value | Use for |
|---|---|---|
IYS_FILTER_INFORMATIONAL |
0 |
Informational messages |
IYS_FILTER_COMMERCIAL_INDIVIDUAL |
11 |
Commercial messages to individuals |
IYS_FILTER_COMMERCIAL_MERCHANT |
12 |
Commercial messages to merchants |
With an İYS filter, NetGsm only delivers to recipients whose consent is recorded in İYS.
OTP messages
NetGsmOtpMessage uses NetGsm's dedicated OTP route, which delivers with priority and needs an OTP package on your
account. An OTP message goes to one recipient, and scheduling, encoding and İYS options don't apply.
use Siberfx\NetGsm\Sms\NetGsmOtpMessage; public function toNetGsm(object $notifiable): NetGsmOtpMessage { return new NetGsmOtpMessage("Your verification code is {$this->code}"); }
Cancelling and sender names
NetGsm::cancel($jobId); // true. Only scheduled messages that haven't been sent can be cancelled. NetGsm::getHeaders(); // ['COMPANY', 'BRAND']
Reports
use Siberfx\NetGsm\Report\NetGsmSmsReport; $report = (new NetGsmSmsReport) ->setStartDate(now()->subDay()->startOfDay()) ->setEndDate(now()->endOfDay()); $rows = NetGsm::getReports($report); // Illuminate\Support\Collection // Same thing, with dates and filters as arguments: $rows = NetGsm::getReports(new NetGsmSmsReport, $start, $end, ['bulkId' => [$jobId]]);
| Method | Description | Type |
|---|---|---|
setStartDate() |
Start date | DateTimeInterface |
setEndDate() |
End date | DateTimeInterface |
setBulkId() |
One or more job ids | string|int|array |
setPageSize() |
Rows per request (1–100, default 100) | int |
setPage() |
Fetch only this page (0 based) | ?int |
Without setPage(), every page is fetched and merged. NetGsm allows about 10 report queries per minute.
Each row looks like this:
[
'jobId' => '17377215342605050417149344',
'phone' => '905051234567',
'status' => 1,
'operatorCode' => 30,
'length' => 1,
'deliveredDate' => '06.10.2026 14:30:12',
'errorCode' => 0,
'referenceId' => '...',
]
Status codes
| Code | Meaning |
|---|---|
| 0 | Pending |
| 1 | Delivered |
| 2 | Expired (validity period passed) |
| 3 | Invalid or restricted number |
| 4 | Not sent to operator |
| 11 | Rejected by operator |
| 12 | Delivery error |
| 13 | Duplicate message |
| 14 | Insufficient balance |
| 15 | Blacklisted number |
| 16 | Rejected by İYS |
| 17 | İYS error |
Operator codes
| Code | Operator |
|---|---|
| 10 | Vodafone |
| 20 | Türk Telekom |
| 30 | Turkcell |
| 40 | Netgsm STH |
| 41 | Netgsm Mobil |
| 60 | Türk Telekom Sabit |
| 70 | Undefined operator |
| 160 | KKTC Vodafone |
| 880 | KKTC Turkcell |
| 212, 213, 214, 215 | International |
Job statistics
NetGsm::getStats($jobId); // [['status' => ..., 'totalMessageLength' => 2, 'totalSms' => 2, // 'chargeStatement' => ..., 'statement' => ..., 'domestic' => true]]
Account balance
NetGsm::getCredit(); // "2,7" (TL, as returned by NetGsm) NetGsm::getAvailablePackages(); // Collection: [ // ['amount' => 1000, 'name' => 'Adet Flash Sms'], // ['amount' => 953, 'name' => 'Adet OTP Sms'], // ]
İYS integration
İYS requests need NETGSM_BRANDCODE.
Add a consent
| Method | Description | Type | Required |
|---|---|---|---|
setRefId() |
Your reference id for the request | string|int |
No |
setType() |
MESAJ, ARAMA or EPOSTA |
string |
Yes |
setSource() |
Consent source, e.g. HS_WEB |
string |
Yes |
setRecipient() |
Phone (+905…) or email address |
string |
Yes |
setStatus() |
ONAY or RET |
string |
Yes |
setConsentDate() |
Consent date | string (Y-m-d H:i:s) |
Yes |
setRecipientType() |
BIREYSEL or TACIR |
string |
Yes |
setRetailerCode() |
Retailer code | ?int |
No |
setRetailerAccess() |
Retailer access | ?int |
No |
use Siberfx\NetGsm\Iys\Requests\Add; $consent = (new Add) ->setRefId(999999) ->setType('MESAJ') ->setSource('HS_WEB') ->setRecipient('+905051234567') ->setStatus('ONAY') ->setConsentDate(now()->toDateTimeString()) ->setRecipientType('BIREYSEL'); $response = NetGsm::iys()->addAddress($consent)->send(); // ['code' => '0', 'error' => 'false', 'uid' => '73113cb9-...']
To add several records in one request, call addAddress() more than once before send():
$iys = NetGsm::iys(); $iys->addAddress((clone $consent)->setType('MESAJ')); $iys->addAddress((clone $consent)->setType('ARAMA')); $response = $iys->send();
Records that fail validation are listed under erroritem in the response.
Search a consent
use Siberfx\NetGsm\Iys\Requests\Search; $search = (new Search) ->setType('MESAJ') ->setRecipient('+905051234567') ->setRecipientType('BIREYSEL'); $response = NetGsm::iys()->searchAddress($search)->send();
Error handling
NetGsm result codes become exceptions with a translated message. getCode() returns the NetGsm code.
use Siberfx\NetGsm\Exceptions\AbstractNetGsmException; use Siberfx\NetGsm\Exceptions\CouldNotSendNotification; try { NetGsm::sendSms($message); } catch (CouldNotSendNotification $e) { report($e); // e.g. code 40: "Message title (sender name) is not defined in system" } catch (AbstractNetGsmException $e) { // any other NetGsm error }
| Exception | Thrown when |
|---|---|
CouldNotSendNotification |
SMS or OTP sending returns an error code |
IncorrectPhoneNumberFormatException |
There's no recipient, or a number is invalid |
NetGsmException |
Report, balance, cancel or another API call fails |
InvalidConfiguration |
The package isn't configured |
All of them extend Siberfx\NetGsm\Exceptions\AbstractNetGsmException.
Common NetGsm result codes
| Code | Meaning |
|---|---|
| 00 | Success |
| 20 | Message text problem, or the message is too long |
| 30 | Invalid credentials, no API access, or request from a non-allowed IP |
| 40 / 41 | Sender name isn't defined or isn't valid |
| 50 / 51 | İYS controlled sending isn't allowed, or no İYS brand found (SMS) |
| 50–52 | Invalid recipient number (OTP) |
| 60 | No OTP package (OTP), no records (reports), or job id not found (cancel) |
| 70 | Invalid or missing parameter |
| 80 | Rate limit exceeded |
| 85 | More than 20 messages to the same number within one minute |
| 100–110 | NetGsm system error |
Testing your application
Use Laravel's notification fake to check that a notification was sent without calling NetGsm:
use Illuminate\Support\Facades\Notification; use Siberfx\NetGsm\NetGsmChannel; Notification::fake(); $user->notify(new OrderShipped($order)); Notification::assertSentTo($user, OrderShipped::class, function ($notification, array $channels) { return in_array(NetGsmChannel::class, $channels); });
When you use the facade directly, mock it:
use Siberfx\NetGsm\NetGsmFacade as NetGsm; NetGsm::shouldReceive('sendSms')->once()->andReturn('123456');
API reference
| Method | Returns | Description |
|---|---|---|
sendSms(AbstractNetGsmMessage $message) |
?string |
Sends an SMS or OTP; returns job id |
cancel(string $jobId) |
bool |
Cancels a scheduled job |
getHeaders() |
string[] |
Sender names on the account |
getReports(AbstractNetGsmReport $report, ...) |
Collection |
Delivery report rows |
getStats(string $jobId) |
Collection |
Statistics for one job |
getCredit() |
?string |
Remaining credit (TL) |
getAvailablePackages() |
Collection |
Package balances |
iys() |
NetGsmIys |
İYS request builder |
Troubleshooting
- Code 30 on every request. Check that you're using an API user (not your panel login) and that API access is enabled for it. If the account has an IP restriction, add your server's IP in the NetGsm panel.
- Code 40. The sender name must be approved in your NetGsm account. List the approved ones with
NetGsm::getHeaders(). - Turkish characters show up wrong. Set
NETGSM_ENCODING=TRor call->setEncoding('TR'). Turkish encoding fits fewer characters into one SMS. - Code 85. NetGsm blocks more than 20 messages to the same number within one minute.
- The phone number is rejected locally. Numbers must be at least 10 digits with no spaces, e.g.
5051234567or905051234567.
Upgrading from 4.x
Version 5 uses NetGsm's REST v2 API and requires PHP 8.4+ and Laravel 12 or 13.
| 4.x | 5.x |
|---|---|
NETGSM_SMS_SENDING_METHOD, setSendMethod() |
Removed; every message is sent as JSON |
NETGSM_LANGUAGE |
NETGSM_ENCODING (TR for Turkish characters) |
setAuthorizedData(true) |
setIysFilter(...) |
NetGsmSmsDetailReport |
NetGsmSmsReport or NetGsm::getStats($jobId) |
setVersion(), setView(), setType() |
Removed; see Reports for the new row format |
Packages: amountType, packageType |
name |
İYS send() returns a JSON string |
Returns an array |
If you published the config file before, publish it again.
Contributing
composer install composer test # PHPUnit composer lint # fix code style with Laravel Pint composer lint:test # check code style
Pull requests are welcome. Please add tests and run Pint before you submit.
Changelog
See CHANGELOG for what changed in each release.
Security
If you discover a security issue, please email info@siberfx.com instead of using the issue tracker.
Credits
License
The MIT License (MIT). See LICENSE for details.