plivo/plivo-php
The Plivo PHP SDK simplifies integrating communications into PHP apps via the Plivo REST API, letting you make voice calls, send SMS, and generate Plivo XML to control call flows. Supports PHP 7.3+ and installs easily with Composer.
The SDK is a plain Composer library, not a Laravel package — there is no service provider, no facade, no publishable config, and no auto-discovery. You wire it up yourself, which takes about five minutes.
1. Install and pin. The README's own example pins 4.14.0 while the package has shipped newer releases, so pin explicitly rather than floating:
composer require plivo/plivo-php:^4.14
Requires PHP 7.3+, so it fits any Laravel 9/10/11 app without constraining your platform.
2. Credentials in config, never in code. The RestClient zero-argument constructor reads PLIVO_AUTH_ID / PLIVO_AUTH_TOKEN straight from the environment. That works, but it bypasses Laravel's config cache — put them in config/services.php and pass them explicitly:
// config/services.php
'plivo' => [
'auth_id' => env('PLIVO_AUTH_ID'),
'auth_token' => env('PLIVO_AUTH_TOKEN'),
'from' => env('PLIVO_FROM'), // your E.164 sender number
'answer_url' => env('PLIVO_ANSWER_URL'), // public XML endpoint
],
3. Bind a singleton. The client is stateless and token-authenticated (no OAuth refresh), so one instance per request/worker is fine:
// App\Providers\AppServiceProvider::register()
$this->app->singleton(\Plivo\RestClient::class, fn () => new \Plivo\RestClient(
config('services.plivo.auth_id'),
config('services.plivo.auth_token'),
));
Where to look first. The README's "The Basics" section defines the whole mental model: every resource exposes create / get / update / delete / list, and list() returns 20 items per page. Then read the resource class for the endpoint you actually need (e.g. Plivo\Resources\Message\MessageInterface) — the README examples are illustrative, not exhaustive, and the authoritative parameter list is the SDK source plus Plivo's REST docs.
First use case to build. A transactional SMS — order shipped, appointment reminder — sent from a queued job. It exercises the three things you'll reuse everywhere: client injection, parameter shape, and error handling.
$message = $client->messages->create([
'src' => config('services.plivo.from'),
'dst' => $order->customer_phone, // E.164, e.g. +14156667777
'text' => "Your order {$order->reference} has shipped.",
]);
Persist the returned message UUID immediately; it's your only handle for status callbacks and support tickets.
Wrap the client behind your own interface. The SDK is a thin HTTP pass-through with no Laravel testing hooks — Http::fake() will not intercept it, because it ships its own HTTP layer. If you inject RestClient directly into controllers you cannot write a fast test. Inject a small interface instead and bind the SDK implementation in the container:
interface SmsGateway {
public function send(string $to, string $text): string; // returns message uuid
}
Then a single PlivoSmsGateway adapter holds every SDK call, and tests bind a fake implementation. This one decision saves you from Mockery gymnastics later.
Queue outbound sends, always. SMS and voice calls are network calls to a third party; a slow Plivo response should never sit in a user's request. A job also gives you retries, backoff, and a natural place to record failures:
final class SendShipmentSms implements ShouldQueue
{
public int $tries = 3;
public array $backoff = [10, 60, 300];
public function handle(SmsGateway $sms): void
{
$uuid = $sms->send($this->order->customer_phone, $this->body);
$this->order->update(['sms_uuid' => $uuid]); // idempotency handle
}
}
Note $tries > 1 on a non-idempotent operation is a duplicate-delivery risk. Either guard on $this->order->sms_uuid being already set, or set $tries = 1 and handle failures explicitly. Plivo does not offer an idempotency key on message creation.
Webhooks: a dedicated, CSRF-exempt route group. Inbound SMS, call state changes and delivery receipts all arrive as POSTs from Plivo. Register them under routes/api.php (already stateless, no CSRF) or add the URI to VerifyCsrfToken::$except. Keep the controller thin — acknowledge fast, push the payload onto a queue:
Route::post('/webhooks/plivo/sms', PlivoSmsWebhookController::class);
Then verify the signature in middleware before the controller ever runs (see Gotchas).
Answer URLs return Plivo XML, not JSON. For voice, Plivo fetches your URL and expects XML built with Plivo\XML\Response. Use the builder rather than hand-writing XML:
public function answer(Request $request)
{
$r = new \Plivo\XML\Response();
$gather = $r->addGather(['action' => route('ivr.menu'), 'numDigits' => 1]);
$gather->addSpeak('Press 1 for sales, 2 for support.');
return response($r->toXML())
->header('Content-Type', 'application/xml');
}
Returning via response() (not echo inside a Blade view) is what keeps the XML declaration on line one — a stray newline before <?xml makes Plivo reject the document.
Multi-account / multi-tenant. If tenants bring their own Plivo credentials, don't bind a singleton. Resolve a RestClient per tenant from encrypted columns and cache it on the tenant model for the request lifetime.
Status callbacks are a separate flow from your own state machine. Point url (messages) or callback_url (calls) at your webhook endpoint and treat those events as advisory. Deliveries can arrive out of order or be retried; key your updates on the message/call UUID and make the handler idempotent.
env() outside config files returns null under config:cache. The README's zero-argument new RestClient() depends on PLIVO_AUTH_ID/PLIVO_AUTH_TOKEN being live environment variables. In a cached-config production deploy you'll get an authentication error that looks like a bad token. Always read from config('services.plivo.*').
Signature verification is the most common integration bug. Plivo signs the request URL plus the POST parameters with HMAC-SHA256 keyed on your auth token (V2), and adds a nonce for V3 (X-Plivo-Signature-V3 / X-Plivo-Signature-V3-Nonce). Three things break it:
$request->all() after Laravel has re-parsed and possibly reordered it. Build the signed string from $request->getContent() in the order received.$request->url() instead of the full URL with query string, or you're behind a proxy and fullUrl() reports http:// where Plivo signed https://. Configure TrustProxies so Laravel sees the real scheme.Log the computed vs. received signature once in staging and move on; don't leave signature checks disabled "temporarily."
Pass JSON payloads as strings, not arrays. Every WhatsApp example in the README — templates, interactive buttons, lists — passes a json_encode'd string. Follow that. The SDK does not reliably re-encode associative arrays for these fields, and an array will either be serialized as Array or silently dropped.
calls->create() is positional; messages->create() is associative. This inconsistency is real and easy to misread:
$client->calls->create($from, [$to], $answerUrl); // positional
$client->messages->create(['src' => ..., 'dst' => ...]); // associative
Pagination stops at 20 and there is no cursor. list() gives you page one; everything beyond requires manually walking limit/offset. For a reconciliation job over thousands of messages, loop until a short page comes back and add a small sleep — this is also where you'll first hit rate limiting.
Catch broadly at the boundary. Plivo\Exceptions\PlivoRestException covers API-level errors (bad number, unapproved template, insufficient balance), but DNS failures, timeouts and TLS errors surface as Guzzle exceptions. In a job's handle(), catch \Throwable, log a redacted payload, and let the queue decide. Log the Plivo error_code/message — support will ask for it.
Error codes are more informative than messages. "Template mismatch" from WhatsApp means your parameter count or names don't match the approved template exactly, including parameter_name vs positional ordering. Keep the template definition next to the code that sends it.
Redact before logging. Auth tokens in headers and customer message bodies are both PII-adjacent. If you enable SDK debug output or dump exception context, strip them; Laravel's Log::withContext is a reasonable place to attach the message UUID only.
README bug worth knowing: the WhatsApp section is copy-pasted from the Go SDK ("how to use the Plivo Go SDK"). The PHP code below it is correct — just don't trust the prose.
PHLO uses a different client and namespace. Plivo\Resources\PHLO\PhloRestClient, not RestClient, and it doesn't zero-arg-read env vars in the documented examples. Don't try to inject it where you bound RestClient.
Response objects aren't arrays. print_r on a create response gives you a resource object with camelCase or snake_case properties depending on the resource. Inspect once with json_encode($response) (most resources serialize), write one small mapper into your own DTO, and never scatter raw property access across the codebase — it's the part most likely to shift between SDK minor versions.
How can I help you explore Laravel packages today?