PHP SDK
The official PHP SDK for Sent provides a clean, object-oriented interface for sending messages. Built with modern PHP 8.1+ features.
Requirements
PHP 8.1.0 or higher.
Installation
composer require sentdm/sent-dm-phpTo install a specific version:
composer require "sentdm/sent-dm-php 0.15.0"Quick Start
Initialize the client
<?php
require_once 'vendor/autoload.php';
use SentDM\Client;
$client = new Client($_ENV['SENT_DM_API_KEY']); // Your API key from the Sent DashboardSend your first message
<?php
require_once 'vendor/autoload.php';
use SentDM\Client;
$client = new Client($_ENV['SENT_DM_API_KEY']);
$result = $client->messages->send(
to: ['+1234567890'],
template: [
'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
'name' => 'welcome',
'parameters' => [
'name' => 'John Doe',
'order_id' => '12345'
]
],
channel: ['sms', 'whatsapp'] // Optional
);
var_dump($result->data->messages[0]->id);
var_dump($result->data->messages[0]->status);Authentication
The client accepts an API key as the first parameter.
use SentDM\Client;
// Using API key directly
$client = new Client('your_api_key');
// Or from environment variable
$client = new Client($_ENV['SENT_DM_API_KEY']);Send Messages
This library uses named parameters to specify optional arguments. Parameters with a default value must be set by name.
Send a message
$result = $client->messages->send(
to: ['+1234567890'],
template: [
'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
'name' => 'welcome',
'parameters' => [
'name' => 'John Doe',
'order_id' => '12345'
]
],
channel: ['sms', 'whatsapp']
);
var_dump($result->data->messages[0]->id);
var_dump($result->data->messages[0]->status);Test mode
Use testMode: true to validate requests without sending real messages:
$result = $client->messages->send(
to: ['+1234567890'],
template: [
'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
'name' => 'welcome'
],
testMode: true // Validates but doesn't send
);
// Response will have test data
var_dump($result->data->messages[0]->id);
var_dump($result->data->messages[0]->status);Handling errors
When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of SentDM\Core\Exceptions\APIException will be thrown:
use SentDM\Core\Exceptions\APIConnectionException;
use SentDM\Core\Exceptions\RateLimitException;
use SentDM\Core\Exceptions\APIStatusException;
try {
$result = $client->messages->send(
to: ['+1234567890'],
template: [
'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
'name' => 'welcome'
]
);
} catch (APIConnectionException $e) {
echo "The server could not be reached", PHP_EOL;
var_dump($e->getPrevious());
} catch (RateLimitException $e) {
echo "A 429 status code was received; we should back off a bit.", PHP_EOL;
} catch (APIStatusException $e) {
echo "Another non-200-range status code was received", PHP_EOL;
echo $e->getMessage();
}Error codes are as follows:
| Cause | Error Type |
|---|---|
| HTTP 400 | BadRequestException |
| HTTP 401 | AuthenticationException |
| HTTP 403 | PermissionDeniedException |
| HTTP 404 | NotFoundException |
| HTTP 409 | ConflictException |
| HTTP 422 | UnprocessableEntityException |
| HTTP 429 | RateLimitException |
| HTTP >= 500 | InternalServerException |
| Other HTTP error | APIStatusException |
| Timeout | APITimeoutException |
| Network error | APIConnectionException |
Retries
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
// Configure the default for all requests:
$client = new Client($_ENV['SENT_DM_API_KEY'], maxRetries: 0);
// Or, configure per-request:
$result = $client->messages->send(
to: ['+1234567890'],
template: [
'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
'name' => 'welcome'
],
requestOptions: ['maxRetries' => 5],
);Value Objects
It is recommended to use the static with constructor and named parameters to initialize value objects.
use SentDM\Models\TemplateDefinition;
$definition = TemplateDefinition::with(
body: [...],
header: [...],
);However, builders are also provided:
$definition = (new TemplateDefinition)->withBody([...]);Contacts
Create and manage contacts:
// Create a contact
$result = $client->contacts->create([
'phoneNumber' => '+1234567890'
]);
var_dump($result->data->id);
// List contacts
$result = $client->contacts->list(['limit' => 100]);
foreach ($result->data->data as $contact) {
echo $contact->phoneNumber . " - " . implode(', ', $contact->availableChannels) . "\n";
}
// Get a contact
$result = $client->contacts->get('contact-uuid');
// Update a contact
$result = $client->contacts->update('contact-uuid', [
'phoneNumber' => '+1987654321'
]);
// Delete a contact
$client->contacts->delete('contact-uuid');Templates
List and retrieve templates:
// List templates
$result = $client->templates->list();
foreach ($result->data->data as $template) {
echo $template->name . " (" . $template->status . "): " . $template->id . "\n";
echo " Category: " . $template->category . "\n";
}
// Get a specific template
$result = $client->templates->get('template-uuid');
echo "Name: " . $result->data->name . "\n";
echo "Status: " . $result->data->status . "\n";Webhooks
Recommended pattern: Webhooks are the primary way to track message delivery — don't poll the API. Save the message ID when you send, then update your database as webhook events arrive.
Sent delivers signed POST requests to your endpoint for every status change. Two event types exist:
messages— Message status changes (SENT,DELIVERED,READ,FAILED, …)templates— WhatsApp template approval/rejection
The signing secret (from the Sent Dashboard) has a whsec_ prefix. Strip it and base64-decode the remainder to obtain the raw HMAC key. The signed content is {X-Webhook-ID}.{X-Webhook-Timestamp}.{rawBody} and the signature format is v1,{base64(hmac)}.
// routes/api.php
Route::post('/webhooks/sent', [WebhookController::class, 'handleSent']);<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
public function handleSent(Request $request)
{
// 1. Read raw body
$payload = $request->getContent();
$webhookId = $request->header('X-Webhook-ID', '');
$timestamp = $request->header('X-Webhook-Timestamp', '');
$signature = $request->header('X-Webhook-Signature', '');
// 2. Verify: signed content = "{webhookId}.{timestamp}.{rawBody}"
$secret = config('services.sent_dm.webhook_secret'); // "whsec_abc123..."
$keyBase64 = str_starts_with($secret, 'whsec_') ? substr($secret, 6) : $secret;
$keyBytes = base64_decode($keyBase64);
$signed = "{$webhookId}.{$timestamp}.{$payload}";
$expected = 'v1,' . base64_encode(hash_hmac('sha256', $signed, $keyBytes, true));
if (!hash_equals($expected, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
// 3. Optional: reject replayed events older than 5 minutes
if (abs(time() - intval($timestamp)) > 300) {
return response()->json(['error' => 'Timestamp too old'], 401);
}
$event = json_decode($payload);
// 4. Handle events — update message status in your own database
if ($event->field === 'messages') {
\DB::table('messages')
->where('sent_id', $event->payload->message_id)
->update(['status' => $event->payload->message_status]);
}
// 5. Always return 200 quickly
return response()->json(['received' => true]);
}
}See the Webhooks reference for the full payload schema and all status values.
Making custom or undocumented requests
Undocumented properties
You can send undocumented parameters to any endpoint using the extra* parameters:
$result = $client->messages->send(
to: ['+1234567890'],
template: [
'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
'name' => 'welcome'
],
requestOptions: [
'extraQueryParams' => ['my_query_parameter' => 'value'],
'extraBodyParams' => ['my_body_parameter' => 'value'],
'extraHeaders' => ['my-header' => 'value'],
],
);Undocumented endpoints
To make requests to undocumented endpoints while retaining the benefit of auth, retries, and so on:
$response = $client->request(
method: 'post',
path: '/undocumented/endpoint',
query: ['dog' => 'woof'],
headers: ['useful-header' => 'interesting-value'],
body: ['hello' => 'world']
);Laravel Integration
Service Provider (example)
// config/app.php
'providers' => [
// ...
App\Providers\SentDMServiceProvider::class,
],
// app/Providers/SentDMServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use SentDM\Client;
class SentDMServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(Client::class, function ($app) {
return new Client(
apiKey: config('services.sent_dm.api_key'),
);
});
}
}// config/services.php
return [
// ...
'sent_dm' => [
'api_key' => env('SENT_DM_API_KEY'),
],
];Dependency Injection
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use SentDM\Client;
class MessageController extends Controller
{
protected $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function send(Request $request)
{
$result = $this->client->messages->send(
to: [$request->input('phone_number')],
template: [
'id' => $request->input('template_id'),
'name' => 'welcome',
'parameters' => $request->input('variables', [])
]
);
if ($result->success) {
return response()->json([
'message_id' => $result->data->messages[0]->id,
'status' => $result->data->messages[0]->status
]);
}
return response()->json(['error' => $result->error->message], 400);
}
}Webhook Handler
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
public function handleSent(Request $request)
{
// 1. Read raw body — do NOT use $request->json() first
$payload = $request->getContent();
$webhookId = $request->header('X-Webhook-ID', '');
$timestamp = $request->header('X-Webhook-Timestamp', '');
$signature = $request->header('X-Webhook-Signature', '');
// 2. Verify: signed content = "{webhookId}.{timestamp}.{rawBody}"
$secret = config('services.sent_dm.webhook_secret'); // "whsec_abc123..."
$keyBase64 = str_starts_with($secret, 'whsec_') ? substr($secret, 6) : $secret;
$keyBytes = base64_decode($keyBase64);
$signed = "{$webhookId}.{$timestamp}.{$payload}";
$expected = 'v1,' . base64_encode(hash_hmac('sha256', $signed, $keyBytes, true));
if (!hash_equals($expected, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
// 3. Optional: reject replayed events older than 5 minutes
if (abs(time() - intval($timestamp)) > 300) {
return response()->json(['error' => 'Timestamp too old'], 401);
}
$event = json_decode($payload);
// 4. Update message status in your own database
if ($event->field === 'messages') {
\DB::table('messages')
->where('sent_id', $event->payload->message_id)
->update(['status' => $event->payload->message_status]);
}
// 5. Always return 200 quickly
return response()->json(['received' => true]);
}
}Symfony Integration
# config/packages/sent_dm.yaml
parameters:
env(SENT_DM_API_KEY): ''
services:
SentDM\Client:
arguments:
$apiKey: '%env(SENT_DM_API_KEY)%'<?php
namespace App\Controller;
use SentDM\Client;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class MessageController
{
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
#[Route('/api/send', methods: ['POST'])]
public function send(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$result = $this->client->messages->send(
to: [$data['phone_number']],
template: [
'id' => $data['template_id'],
'name' => 'welcome'
]
);
if ($result->success) {
return new JsonResponse([
'message_id' => $result->data->messages[0]->id,
'status' => $result->data->messages[0]->status
]);
}
return new JsonResponse(['error' => $result->error->message], 400);
}
}Source & Issues
- Version: 0.15.0
- GitHub: sentdm/sent-dm-php
- Packagist: sentdm/sent-dm-php
- Issues: Report a bug
Getting Help
- Documentation: API Reference
- Troubleshooting: Common Issues
- Support: Email support@sent.dm with your request ID