Laravel
Send SMS from a Laravel application with Nishchit — a config entry, a small client, a notification channel, queued sending and a webhook route with signature verification.
There is no Nishchit PHP package to install. The API is one endpoint for sending and six for reading, and Laravel's HTTP client already does everything a package would — so this page wires it up directly rather than putting a dependency between you and eleven lines of code.
Which Laravel
The code below targets Laravel 11 and 12. The only version-sensitive part is CSRF exemption for
the webhook route, which moved to bootstrap/app.php in Laravel 11; the note in that section
covers both shapes.
Configure
Put the key in .env. Use the nk_test_ key while building — it never reaches a carrier and
never spends a credit.
NISHCHIT_KEY=nk_test_your_key_here
NISHCHIT_BASE_URL=https://api.nishchit.tech
NISHCHIT_WEBHOOK_SECRET=whsec_your_secret_hereThen config/services.php:
'nishchit' => [
'key' => env('NISHCHIT_KEY'),
'base_url' => env('NISHCHIT_BASE_URL', 'https://api.nishchit.tech'),
'webhook_secret' => env('NISHCHIT_WEBHOOK_SECRET'),
],A client
One class. app/Services/Nishchit.php:
<?php
namespace App\Services;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class Nishchit
{
protected function client(): PendingRequest
{
return Http::withToken(config('services.nishchit.key'))
->baseUrl(config('services.nishchit.base_url'))
->acceptJson()
->timeout(15)
->connectTimeout(5);
}
public function send(string $to, string $body, ?string $from = null, ?string $idempotencyKey = null): array
{
$response = $this->client()
->withHeaders(['Idempotency-Key' => $idempotencyKey ?? (string) Str::uuid()])
->post('/v1/messages', array_filter([
'to' => $to,
'from' => $from,
'body' => $body,
]));
return $response->json();
}
public function preview(string $body, int $recipients = 1): array
{
return $this->client()
->post('/v1/pricing/preview', ['body' => $body, 'recipients' => $recipients])
->json();
}
public function balance(): array
{
return $this->client()->get('/v1/balance')->json();
}
}Always send an Idempotency-Key
Laravel's retry() helper and your queue worker will both re-issue a failed POST. Without an
idempotency key that is a second SMS at a second charge. With one, Nishchit returns the stored
first response and sets Idempotency-Replayed: true. Derive the key from the thing you are
notifying about — order-8841-shipped — rather than a random UUID, so a retry from a different
process still collides correctly.
Send one message
use App\Services\Nishchit;
$message = app(Nishchit::class)->send(
to: '+8801712345678',
body: 'Your Nishchit OTP is 482913',
idempotencyKey: 'signup-otp-user-9281',
);
// msg_01JBX7QW9K2M5T8N4V6C3Z1H0A accepted
logger()->info($message['id'], ['status' => $message['status']]);status comes back as accepted, which means Nishchit has taken the message — not that a
handset received it. See delivery status before you render anything to
a user.
As a notification channel
The idiomatic Laravel shape. app/Notifications/Channels/NishchitChannel.php:
<?php
namespace App\Notifications\Channels;
use App\Services\Nishchit;
use Illuminate\Notifications\Notification;
class NishchitChannel
{
public function __construct(protected Nishchit $nishchit) {}
public function send(mixed $notifiable, Notification $notification): void
{
$message = $notification->toNishchit($notifiable);
$to = $notifiable->routeNotificationFor('nishchit', $notification);
if (! $to) {
return;
}
$this->nishchit->send(
to: $to,
body: $message['body'],
idempotencyKey: $message['idempotency_key'] ?? null,
);
}
}Register it in a service provider, then a notification looks like any other:
<?php
namespace App\Notifications;
use App\Notifications\Channels\NishchitChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class OrderShipped extends Notification
{
use Queueable;
public function __construct(protected string $orderId) {}
public function via(mixed $notifiable): array
{
return [NishchitChannel::class];
}
public function toNishchit(mixed $notifiable): array
{
return [
'body' => "Your order {$this->orderId} has shipped.",
'idempotency_key' => "order-{$this->orderId}-shipped",
];
}
}On the notifiable model:
public function routeNotificationForNishchit(): ?string
{
return $this->phone;
}Adding implements ShouldQueue to the notification moves the send onto a queue worker, which is
what you want for anything that is not a passcode — a passcode should go out on the request.
Count segments before you send
Bangla is billed at 70 characters per segment rather than 160, so a notification body that looks
short can be three segments. POST /v1/pricing/preview is authoritative:
$preview = app(Nishchit::class)->preview(
body: 'হ্যালো করিম, আপনার অর্ডার ৮৮৪১ পাঠানো হয়েছে।',
recipients: 5000,
);
// ucs2 2 10000
logger()->info('cost', [
'encoding' => $preview['encoding'],
'segments' => $preview['segments'],
'credits' => $preview['total_credits'],
]);Run a campaign body through this before the campaign, not after. Bangla SMS explains why, and the segment calculator does the same arithmetic in a browser.
Handle errors by code
Every failure carries error.code and a doc_url pointing at the anchor for that exact code.
Branch on the code, never on the message text.
use Illuminate\Http\Client\Response;
$response = Http::withToken(config('services.nishchit.key'))
->baseUrl(config('services.nishchit.base_url'))
->post('/v1/messages', [...]);
if ($response->failed()) {
$error = $response->json('error');
match ($error['code']) {
'insufficient_credits' => $this->alertOps($error),
'invalid_recipient', 'unsupported_country' => $this->markUnreachable($error),
'content_rejected' => $this->flagForReview($error),
'rate_limit_exceeded' => $this->release((int) $response->header('Retry-After')),
default => report(new \RuntimeException($error['code'].': '.$error['message'])),
};
}The full list with causes and fixes is on errors.
Receive webhooks
Nishchit signs every delivery with HMAC-SHA256 over {timestamp}.{raw body} and sends it in a
Nishchit-Signature header as t=…,v1=….
Route::post('/webhooks/nishchit', function (Request $request) {
$header = $request->header('Nishchit-Signature', '');
$raw = $request->getContent();
$parts = [];
foreach (explode(',', $header) as $pair) {
[$k, $v] = array_pad(explode('=', $pair, 2), 2, null);
$parts[$k] = $v;
}
if (empty($parts['t']) || empty($parts['v1'])) {
abort(400, 'invalid signature');
}
if (abs(time() - (int) $parts['t']) > 300) {
abort(400, 'stale signature');
}
$expected = hash_hmac(
'sha256',
$parts['t'].'.'.$raw,
config('services.nishchit.webhook_secret'),
);
if (! hash_equals($expected, $parts['v1'])) {
abort(400, 'invalid signature');
}
ProcessNishchitEvent::dispatch($request->json()->all());
return response()->noContent();
})->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);Sign over the raw body, and exempt the route from CSRF
$request->getContent() returns the bytes as sent. Using $request->all() and re-encoding
changes whitespace and key order, and the signature will never match.
The route also has to be exempt from CSRF. The withoutMiddleware call above works everywhere;
in Laravel 11 and 12 you can instead add it to validateCsrfTokens(except: [...]) in
bootstrap/app.php, and in Laravel 10 to the $except array in VerifyCsrfToken.
Return quickly and do the work on a queue. Nishchit retries on a non-2xx, so a handler that processes inline and times out will be delivered the same event again. Webhooks covers the event list, retry schedule and secret rotation.
Test without sending
A nk_test_ key never reaches a carrier and never spends a credit, so point your testing
environment at one in phpunit.xml:
<env name="NISHCHIT_KEY" value="nk_test_your_key_here"/>Test mode also has magic recipient numbers that force specific outcomes — a terminal failure, a gateway outage — so you can exercise your error branches deterministically. They are listed in test mode.
For tests that should not touch the network at all, fake it:
use Illuminate\Support\Facades\Http;
Http::fake([
'api.nishchit.tech/v1/messages' => Http::response([
'id' => 'msg_01JBX7QW9K2M5T8N4V6C3Z1H0A',
'status' => 'accepted',
'delivery_status' => 'unknown',
'segments' => 1,
'credits' => 1,
], 201),
]);Note the delivery_status in that fixture. On the primary Bangladesh route it stays unknown
permanently, so a fake that returns delivered will let you ship a UI that cannot work in
production.
Next
SDKs and tooling
What is generated from the OpenAPI document, what is available today, and how to call the API cleanly from Node, Python, PHP, Go or anything with an HTTP client.
Node.js
Send SMS from Node with Nishchit — a typed client over fetch, idempotent retries, an Express webhook route with constant-time signature verification, and types generated from the published OpenAPI document.