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 ...orx-api-keyon every request. - Generate one
Idempotency-Keyper mutation to avoid double sends. - Treat
202 Acceptedas queued acceptance, then follow delivery through webhooks or DLR reports.
Guzzle
Example with Guzzle.
bash
composer require guzzlehttp/guzzlephp
<?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
| Variable | Example | Description |
|---|---|---|
| COUSSEMA_API_KEY | csm_live_xxx | API key generated from the Coussema dashboard. It stays on the server side. |
| COUSSEMA_BASE_URL | https://api.coussema.com | Public production host. The examples call /v1/sms/send. |
| COUSSEMA_WEBHOOK_SECRET | csm_whsec_xxx | Secret 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.
| Field | Value |
|---|---|
| HTTP | 202 Accepted |
| queued | true |
| status | accepted |
| message.id | Durable Coussema UUID |
| provider.messageId | always 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
clientReferenceto find the OTP, order or payment on your side. - On a network timeout, retry with the same
Idempotency-Keyif you do not know the result.