# Laravel (/laravel)



`sendkuy/laravel` builds on the PHP SDK. It configures the client from `.env`, adds a `sendkuy` notification channel, and turns signed webhooks into Laravel events.

## Install [#install]

```bash
composer require sendkuy/laravel
```

```bash title=".env"
SENDKUY_API_KEY=sk_live_YOUR_API_KEY
```

The service provider and the `SendKuy` facade register themselves.

## Send from anywhere [#send-from-anywhere]

```php
use SendKuy\Laravel\Facades\SendKuy;

$messages = SendKuy::messages()->send([
    'channel' => 'whatsapp',
    'to' => ['+6281234567890'],
    'body' => 'Your order is on its way.',
]);
```

Type-hint `SendKuy\SendKuy` to have the same client injected. Every endpoint has a method; see the [list of methods](/sdks#methods).

## Notifications [#notifications]

```php
use Illuminate\Notifications\Notification;
use SendKuy\Laravel\Notifications\SendKuyMessage;

class OrderShipped extends Notification
{
    public function via(object $notifiable): array
    {
        return ['sendkuy'];
    }

    public function toSendKuy(object $notifiable): SendKuyMessage
    {
        return SendKuyMessage::whatsapp('Order #1042 is on its way.')
            ->media('https://example.com/receipts/1042.pdf', 'document');
    }
}
```

The channel asks the notifiable where to send:

```php
public function routeNotificationForSendKuy(Notification $notification): string
{
    return $this->phone;
}
```

Return an array to reach several recipients in one message call. Or send without a model: `Notification::route('sendkuy', '+6281234567890')->notify(new OrderShipped);`. A notifiable without a recipient throws `MissingRecipientException` before any call is made.

| Builder                                                                       | Sets                           |
| ----------------------------------------------------------------------------- | ------------------------------ |
| `SendKuyMessage::whatsapp($body)`, `::sms($body)`, `::email($subject, $body)` | the channel and the text       |
| `->template($id, $parameters)`                                                | a WhatsApp template            |
| `->media($url, $type, $filename)`                                             | one WhatsApp attachment        |
| `->attach($url, $filename)`                                                   | an email attachment, up to ten |
| `->gateway($id)`                                                              | the exact sender               |
| `->scheduleAt($dateTime)`                                                     | send later                     |

### A retried queued notification is never sent twice [#a-retried-queued-notification-is-never-sent-twice]

When a notification implements `ShouldQueue` and its job may run more than once (`tries` above 1, or `--tries` on the worker), Laravel runs it again after a worker dies, a timeout or an exception. The channel derives the [idempotency key](/idempotency) from Laravel's notification id, which stays the same across every retry of that job, so SendKuy answers a retry with the first result instead of sending again.

Keep `toSendKuy()` deterministic for this to hold: build the message from the notification's own data, not from `now()` or other values that change between runs. A retry that produces a different payload under the same key is refused with `409 idempotency_key_reused`, so the job fails instead of sending twice. To schedule, store the time on the notification when you create it and pass that to `scheduleAt()`.

A refusal is final for that notification. When SendKuy refuses the send, for example with `insufficient_credits` or `no_sender_available`, every retry of the job gets the same refusal under the same key and the job fails. Once you have fixed the cause, send the notification again: a new notification has a new id, and so a new key. Retrying does help with `429` and server errors, which are [not remembered](/idempotency#what-is-remembered).

The channel returns the created messages; a `NotificationSent` listener reads them from `$event->response`.

## Webhooks [#webhooks]

The package registers `POST /sendkuy/webhook` outside the `web` middleware group, so CSRF does not apply. Point a [webhook](/webhooks) at it and store the secret:

```bash title=".env"
SENDKUY_WEBHOOK_SECRET=whsec_...
```

A delivery with a valid signature becomes a `WebhookReceived` event and answers `200`; anything else answers `403`. Without `SENDKUY_WEBHOOK_SECRET` the route answers `500` and logs an error, so the problem shows up in your delivery history.

```php
use Illuminate\Contracts\Queue\ShouldQueue;
use SendKuy\Laravel\Events\WebhookReceived;

class HandleSendKuyWebhook implements ShouldQueue
{
    public function handle(WebhookReceived $event): void
    {
        if ($event->type === 'message.received') {
            // $event->data is the conversation message; $event->id is X-SendKuy-Event-Id
        }
    }
}
```

Queue the listener so the route answers within the 10-second timeout. `$event->id` is the event id from `X-SendKuy-Event-Id`, which stays the same across every retry of one event: store it and skip ids you have seen. `$event->payload` holds the whole verified body. New event types reach the listener without a package update.

## Configuration [#configuration]

```bash
php artisan vendor:publish --tag=sendkuy-config
```

| Key                 | Env                         | Default                                                                                     |
| ------------------- | --------------------------- | ------------------------------------------------------------------------------------------- |
| `api_key`           | `SENDKUY_API_KEY`           | none                                                                                        |
| `base_url`          | `SENDKUY_BASE_URL`          | `https://app.sendkuy.com/api/v1`; change it only to reach a proxy or a mock server in tests |
| `timeout`           | `SENDKUY_TIMEOUT`           | `30` seconds                                                                                |
| `max_retries`       | `SENDKUY_MAX_RETRIES`       | `2`                                                                                         |
| `webhook.secret`    | `SENDKUY_WEBHOOK_SECRET`    | none                                                                                        |
| `webhook.path`      | `SENDKUY_WEBHOOK_PATH`      | `sendkuy/webhook`; empty turns the route off                                                |
| `webhook.tolerance` | `SENDKUY_WEBHOOK_TOLERANCE` | `300` seconds                                                                               |

## Testing [#testing]

`Notification::fake()` works as usual. To fake the client itself, bind your own instance with a PSR-18 client:

```php
$this->app->instance(\SendKuy\SendKuy::class, new \SendKuy\SendKuy('sk_test_123', [
    'http_client' => $fakeClient,
]));
```
