Skip to content
NishchitDOCS
Integrations

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_here

Then 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

Ready to send one?

Signup is self-serve and a test key never touches a carrier or a credit.

NISHCHIT / SMS INFRASTRUCTURE · BANGLADESH/LLMS.TXTPRICINGSTATUSTERMSPRIVACY