Search by

sendkuy / sdk

sendkuy

WhatsApp API, SMS gateway and email API for PHP: send messages and OTP codes and receive replies with SendKuy.

v1.2.0 2026-10-29 04:47 UTC

This package is auto-updated.

Last update: 2026-10-29 04:49:28 UTC


README

SendKuy PHP SDK

SendKuy PHP SDK

Send WhatsApp, SMS and email from PHP with one API key, and retry without ever sending twice.

Latest version Downloads CI PHPStan level max License: MIT

Baca dalam Bahasa Indonesia

What you can send

  • WhatsApp from a number connected by QR code, the SendKuy WA Gateway Chrome extension, or the WhatsApp Cloud API (WhatsApp API).
  • SMS through an Android phone running SendKuy Gateway, or an SMS provider (SMS gateway).
  • Email from your own domain (email API).
  • OTP codes with verifications(), falling back from WhatsApp to SMS.
  • Bulk sends to several recipients or a batch of up to 500, paced per number for you.
  • Two-way conversations for your helpdesk: incoming messages arrive as message.received webhooks.

Install

composer require sendkuy/sdk

PHP 8.2 or newer. Building on Laravel? Install sendkuy/laravel instead: it adds a facade, a notification channel and a signed webhook route on top of this package.

Quickstart

use SendKuy\SendKuy;

$sendkuy = new SendKuy(getenv('SENDKUY_API_KEY'));

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

echo $messages[0]->id; // msg_...

Create a key in the SendKuy dashboard under Developers → Keys. Numbers go in E.164 form, with the leading +. You get one message back per recipient.

Every endpoint has a method

Field names are the API's own, so every example in the API reference maps one to one onto this SDK.

Group Methods
account() get, usage
messages() list, send, sendBatch, get, cancel, resend
numbers() check
verifications() create, get, check
jobs() get
contacts() list, create, bulkImport, lookup, get, update, delete, listAttributes
groups() list, create, get, update, delete, addContacts, removeContacts
templates() list, create, get, update, delete
gateways() list, get
webhooks() list, create, get, update, delete, listDeliveries
campaigns() list, create, get, update, delete, listDispatches, start, pause, resume, cancel
conversations() list, get, listMessages, markRead, reply
workflows() list, get, listExecutions, trigger
executions() get

Objects come back with their fields as properties ($message->status) and as array keys ($message['status']), and your editor autocompletes them. An endpoint newer than your SDK version is still one call away:

$response = $sendkuy->request('GET', '/account');

Retries and idempotency

A timeout tells you nothing about whether a message went out. That is why every write this SDK sends carries an Idempotency-Key, and every retry of that call reuses it: if the first attempt did go through, SendKuy answers the retry with the first result instead of sending again.

The SDK retries connection errors, timeouts, 429 (waiting for Retry-After when it is a minute or less), 5xx and 409 idempotency_request_in_progress, up to twice, with exponential backoff.

When your own code retries, for example a queued job, pass a key derived from something stable in your system:

$sendkuy->messages()->send($params, ['idempotency_key' => 'order-1042-shipped']);
Option Default
base_url https://app.sendkuy.com/api/v1 change it only to reach a proxy or a mock server in tests
timeout 30 seconds; also per request: ['timeout' => 10]
max_retries 2
http_client Guzzle any PSR-18 client

Pagination

foreach ($sendkuy->contacts()->list(['limit' => 100])->autoPaginate() as $contact) {
    echo $contact->id;
}

$page = $sendkuy->contacts()->list();
$page->data;       // this page
$page->hasMore;
$page->nextPage(); // null on the last page

Errors

Every failure is a SendKuy\Exceptions\SendKuyException. Branch on the stable code, never on the message:

use SendKuy\Exceptions\InvalidRequestException;
use SendKuy\Exceptions\RateLimitException;
use SendKuy\Exceptions\SendKuyException;

try {
    $sendkuy->messages()->send($params);
} catch (InvalidRequestException $e) {
    $e->getErrorCode(); // validation_failed
    $e->getParam();     // to
} catch (RateLimitException $e) {
    $e->getRetryAfter(); // seconds
} catch (SendKuyException $e) {
    $e->getRequestId(); // quote it when you contact support
}
Exception When
AuthenticationException the key is missing, wrong, revoked or expired
AuthorizationException the key lacks a scope, or the plan lacks the channel
InvalidRequestException validation, not found, idempotency conflicts
RateLimitException too many requests
ApiException SendKuy failed on its side
ConnectionException SendKuy could not be reached

Webhooks

SendKuy signs every webhook. Verify it against the raw body, before any JSON parsing:

use SendKuy\Exceptions\SignatureVerificationException;
use SendKuy\Webhook;

try {
    $event = Webhook::verify(
        file_get_contents('php://input'),
        $_SERVER['HTTP_X_SENDKUY_SIGNATURE'] ?? '',
        getenv('SENDKUY_WEBHOOK_SECRET'),
    );
} catch (SignatureVerificationException $e) {
    http_response_code(403);
    exit;
}

$deliveryId = $_SERVER['HTTP_X_SENDKUY_EVENT_ID'] ?? ''; // the same on every retry: use it to skip duplicates

if ($event['event'] === 'message.received') {
    $message = $event['data']; // from, channel, body, provider_message_id
}

http_response_code(200);

Use the endpoint secret in full, whsec_ included, and answer within 10 seconds. Slow work belongs in a queue.

message.received carries who wrote (from), the channel, the number that received it and any media. When your account forwards incoming messages to your webhook only, nothing is stored in SendKuy and id and conversation_id are null: reply with messages()->send() to $message['from'] on $message['channel'].

Testing your code

Pass any PSR-18 client and the SDK sends through it:

$sendkuy = new SendKuy('sk_test_123', ['http_client' => $fakeClient]);

A client you pass in owns its own timeouts.

Links

License

MIT. See LICENSE.