PHP SDK

PHP SDK

The official PHP SDK for Sent LogoSent 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-php

To 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 Dashboard

Send 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:

CauseError Type
HTTP 400BadRequestException
HTTP 401AuthenticationException
HTTP 403PermissionDeniedException
HTTP 404NotFoundException
HTTP 409ConflictException
HTTP 422UnprocessableEntityException
HTTP 429RateLimitException
HTTP >= 500InternalServerException
Other HTTP errorAPIStatusException
TimeoutAPITimeoutException
Network errorAPIConnectionException

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

Getting Help

On this page