PHP

Send SMS from PHP or Laravel through the REST API.

Server prerequisites

The API key must stay on the PHP server. Never expose it in an HTML page, a mobile app or public JavaScript.

  • Install Guzzle if your project uses Composer.
  • Put the API key in .env, never in a Blade template or public JavaScript.
  • Send Authorization: Bearer ... or x-api-key on every request.
  • Generate one Idempotency-Key per mutation to avoid double sends.
  • Treat 202 Accepted as queued acceptance, then follow delivery through webhooks or DLR reports.

Guzzle

Example with Guzzle.

bash
composer require guzzlehttp/guzzle
php
<?php

require __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$apiKey = getenv('COUSSEMA_API_KEY');

if (!$apiKey) {
    throw new RuntimeException('COUSSEMA_API_KEY manquant.');
}

$client = new Client([
    'base_uri' => getenv('COUSSEMA_BASE_URL') ?: 'https://api.coussema.com',
    'timeout' => 15,
]);

try {
    $response = $client->post('/v1/sms/send', [
        'headers' => [
            'Authorization' => 'Bearer ' . $apiKey,
            'Content-Type' => 'application/json',
            'Idempotency-Key' => bin2hex(random_bytes(16)),
            'X-Request-Id' => 'php-send-' . date('YmdHis'),
        ],
        'json' => [
            'to' => '+243980395067',
            'message' => 'Votre code : 4829',
            'senderName' => 'MonApp',
            'clientReference' => 'otp-login-4829',
        ],
    ]);

    $payload = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);

    if (($payload['queued'] ?? false) !== true || ($payload['status'] ?? null) !== 'accepted') {
        throw new RuntimeException('SMS non accepté par Coussema.');
    }

    echo 'SMS mis en file. Quota restant: ' . ($payload['remainingQuota'] ?? 'n/a');
} catch (RequestException $e) {
    $body = $e->getResponse() ? (string) $e->getResponse()->getBody() : '';
    throw new RuntimeException('Erreur API Coussema: ' . $body, 0, $e);
}

Environment variables

VariableExampleDescription
COUSSEMA_API_KEYcsm_live_xxxAPI key generated from the Coussema dashboard. It stays on the server side.
COUSSEMA_BASE_URLhttps://api.coussema.comPublic production host. The examples call /v1/sms/send.
COUSSEMA_WEBHOOK_SECRETcsm_whsec_xxxSecret received when creating a webhook, to store in .env.

API response

POST /v1/sms/send answers with a 2xx success once Coussema has validated, stored and queued the SMS. Final delivery then arrives by webhook or DLR report.

FieldValue
HTTP202 Accepted
queuedtrue
statusaccepted
message.idDurable Coussema UUID
provider.messageIdalways null

cURL and Laravel

php
<?php

$payload = [
    'to' => '+243980395067',
    'message' => 'Votre code : 4829',
    'senderName' => 'MonApp',
    'clientReference' => 'otp-login-4829',
];

$ch = curl_init('https://api.coussema.com/v1/sms/send');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 15,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('COUSSEMA_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($body === false) {
    throw new RuntimeException('Erreur réseau: ' . curl_error($ch));
}

curl_close($ch);

if ($status < 200 || $status >= 300) {
    throw new RuntimeException('Erreur API Coussema HTTP ' . $status . ': ' . $body);
}

$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
php
<?php

namespace App\Http\Controllers;

use GuzzleHttp\Client;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class CoussemaSmsController
{
    public function sendOtp(Request $request): JsonResponse
    {
        $request->validate([
            'phone' => ['required', 'string'],
        ]);

        $code = random_int(1000, 9999);

        $client = new Client([
            'base_uri' => config('services.coussema.base_url', 'https://api.coussema.com'),
            'timeout' => 15,
        ]);

        $response = $client->post('/v1/sms/send', [
            'headers' => [
                'Authorization' => 'Bearer ' . config('services.coussema.key'),
                'Content-Type' => 'application/json',
                'Idempotency-Key' => (string) Str::uuid(),
                'X-Request-Id' => 'laravel-otp-' . Str::uuid(),
            ],
            'json' => [
                'to' => $request->input('phone'),
                'message' => 'Votre code : ' . $code,
                'senderName' => 'MonApp',
                'clientReference' => 'otp-' . now()->timestamp,
            ],
        ]);

        $payload = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);

        return response()->json([
            'accepted' => ($payload['queued'] ?? false) === true,
            'status' => $payload['status'] ?? null,
            'request_id' => $response->getHeaderLine('x-request-id'),
        ], $response->getStatusCode());
    }
}
php
'coussema' => [
    'key' => env('COUSSEMA_API_KEY'),
    'base_url' => env('COUSSEMA_BASE_URL', 'https://api.coussema.com'),
    'webhook_secret' => env('COUSSEMA_WEBHOOK_SECRET'),
],

Verify a webhook in PHP

Webhooks confirm message.delivered and message.failed without polling the API in a loop.

php
<?php

$rawBody = file_get_contents('php://input') ?: '';
$timestamp = $_SERVER['HTTP_X_COUSSEMA_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_COUSSEMA_SIGNATURE'] ?? '';
$secret = getenv('COUSSEMA_WEBHOOK_SECRET');

if (!$timestamp || !$signature || !$secret) {
    http_response_code(400);
    exit('Signature manquante.');
}

$signedPayload = $timestamp . '.' . $rawBody;
$expected = 'v1=' . hash_hmac('sha256', $signedPayload, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Signature invalide.');
}

$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);

// Exemple: message.delivered ou message.failed
// Mettez à jour votre commande, paiement, ticket ou session OTP ici.

http_response_code(204);

Sending rules

  • Do not test strictly for HTTP 200: accept 2xx successes that follow the contract.
  • Keep your own clientReference to find the OTP, order or payment on your side.
  • On a network timeout, retry with the same Idempotency-Key if you do not know the result.