PHP and Laravel SDK
Last updated
whisperr/php sends events and identify calls from a PHP backend, with a
service provider and facade for Laravel. Use it for facts only your server
knows, such as payment failures, cancellations and plan changes, and to
confirm contact consent with the server key.
Install
Section titled “Install”composer require whisperr/phpThe package is whisperr/php
on Packagist. The current version is 0.3.0. It needs PHP 8.0 or later with
ext-curl and ext-json, and has no Composer dependencies.
Laravel
Section titled “Laravel”Package discovery registers the service provider and the Whisperr facade.
Set the server key in .env:
WHISPERR_API_KEY=wrk_your_server_keyWHISPERR_APP_VERSION=2026.10.1 # optional: your backend releaseTo edit the config file, publish it:
php artisan vendor:publish --tag=whisperr-configuse Whisperr\Laravel\Facades\Whisperr;
// In a controller: pass the user id explicitly.Whisperr::track((string) auth()->id(), 'plan_upgraded', ['plan' => 'pro']);
// In a webhook or job: use the user id from your own data.Whisperr::track((string) $subscription->user_id, 'subscription_cancelled', ['reason' => 'too_expensive']);
// Traits and a contact the user agreed to:Whisperr::identify((string) $user->id, [ 'traits' => ['first_name' => $user->first_name, 'plan' => 'pro'], 'channels' => [ [ 'type' => 'email', 'address' => $user->email, 'opted_in' => $user->email_consent, 'verified' => $user->hasVerifiedEmail(), ], ],]);The Laravel integration buffers events during the request and sends them in
the app’s terminating hook, after the response is sent.
Laravel config keys (config/whisperr.php):
| Key | Environment variable | Default |
|---|---|---|
api_key | WHISPERR_API_KEY | "" |
base_url | WHISPERR_BASE_URL | https://api.whisperr.net |
app_version | WHISPERR_APP_VERSION | — |
disabled | WHISPERR_DISABLED | false |
debug | WHISPERR_DEBUG | false |
Create the server key as a Server key in Settings → API keys.
Plain PHP
Section titled “Plain PHP”use Whisperr\Whisperr;
$whisperr = new Whisperr(['api_key' => getenv('WHISPERR_API_KEY')]);
$whisperr->track('user_8842', 'subscription_cancelled', ['reason' => 'too_expensive']);
$whisperr->flush(); // the client also flushes at shutdowntrack() and identify() always take the user id first. Pass the same id
your app passes to identify(), and backend and frontend events land on
one timeline. Event names must be
lowercase snake_case; the SDK drops any other name and reports it to
on_error as dropped.
How do I confirm contact consent with the PHP SDK?
Section titled “How do I confirm contact consent with the PHP SDK?”The PHP SDK runs with the server key, so its identify() is how you confirm
consent. Send 'opted_in' => true only when the user agreed to messages on
that channel, and 'verified' => true only for an email address your app
confirmed. Send 'opted_in' => false to record an opt-out.
See Contact consent.
How do I record an event only once?
Section titled “How do I record an event only once?”Cron jobs and payment callbacks can run twice. Pass a stable message ID and
the original time to track(), and Whisperr keeps one event per message ID
for each app:
Whisperr::track( (string) $invoice->user_id, 'billing_issue_started', ['has_access' => true], messageId: "stripe_invoice_{$invoice->id}_billing_issue_started", occurredAt: $invoice->failed_at->toIso8601ZuluString(), // e.g. 2026-10-12T08:00:00Z);- Build the message ID from your own data, not from the current time. It must be non-empty, have no leading or trailing spaces, and be at most 200 bytes.
occurredAtmust be RFC 3339 with a time zone (Zor+04:00). Whisperr accepts times up to 30 days in the past and 5 minutes in the future.- An invalid message ID or time drops the event and reports
dropped.track()never throws. - Without these arguments,
track()uses a random ID and the current time.
How do I set the message language from PHP?
Section titled “How do I set the message language from PHP?”Pass the language the user picked in your product as the app_language
trait. Use a BCP 47 tag such as de or pt-BR. The PHP SDK has no separate
method for it and never guesses a language.
$whisperr->identify((string) $user->id, ['traits' => ['app_language' => 'pt-BR']]);What the PHP SDK adds to every event
Section titled “What the PHP SDK adds to every event”context key | Value |
|---|---|
sdk_name | whisperr-php |
sdk_version | Whisperr::VERSION, for example 0.3.0 |
app_version | The app_version option. Empty: no key. |
A key you pass in the $context argument of track() wins over these.
How do I send billing events from PHP?
Section titled “How do I send billing events from PHP?”When Whisperr has no billing source connected for your app (such as
RevenueCat), send membership changes from your
backend with the reserved names in Whisperr\BillingEvent. Never send them
from the browser or the app. BillingEvent::properties() builds the money
properties and puts the event’s own properties from extra next to them:
use Whisperr\BillingEvent;
$whisperr->track((string) $payment->user_id, BillingEvent::RENEWAL_SUCCEEDED, BillingEvent::properties( planId: 'pro_monthly', price: 9.99, // major units: 9.99, not 999 cents currency: 'usd', // upper-cased to 'USD' planPeriod: 'P1M', // ISO 8601 duration transactionId: (string) $payment->id, store: 'stripe', extra: ['paid_periods_count' => 2, 'current_period_ends_at' => '2026-12-12T08:00:00Z'],), messageId: "renewal_{$payment->id}", occurredAt: $payment->paid_at->toIso8601ZuluString());- An argument you leave out adds no property.
properties()throwsInvalidArgumentExceptionwhencurrencyis not three letters, orpriceis negative or not a finite number.- The constants are
TRIAL_STARTED,TRIAL_CONVERTED,TRIAL_AUTO_RENEW_DISABLED,MEMBERSHIP_STARTED,RENEWAL_SUCCEEDED,AUTO_RENEW_DISABLED,AUTO_RENEW_ENABLED,MEMBERSHIP_ENDED,BILLING_ISSUE_STARTED,BILLING_RECOVEREDandREFUND_GRANTED(BillingEvent::ALLlists them). The Node SDK page lists the properties of each event and the closed value lists.
Register these event names for your app; setup with your AI agent registers the events your app sends. Whisperr stores an event with an unregistered name but does not map it to your app’s events.
How does the PHP SDK deliver events?
Section titled “How does the PHP SDK deliver events?”- Events buffer in memory and go out in batches on
flush(): after the Laravel response, whenflush_atevents are buffered, or at shutdown. - Events go to
POST /v1/events/batchand identify calls toPOST /v1/identify, in order, with theX-API-Keyheader. 429,5xxand network errors are retried up tomax_retriestimes. The wait between tries is at most about 2 seconds, so request teardown does not hang. The PHP SDK does not readRetry-After.- On
401,403or exhausted retries, the batch stays buffered for the nextflush()in the same process. Other4xxresponses drop the batch and reportdropped. - The buffer lives only as long as the PHP process or request.
See Delivery, retries, idempotency.
Options (plain PHP constructor)
Section titled “Options (plain PHP constructor)”| Key | Default | Notes |
|---|---|---|
api_key | — | Server key (wrk_…). Required unless disabled. |
base_url | https://api.whisperr.net | Ingestion base URL. |
app_version | — | Your backend release. Sent as context.app_version. |
flush_at | 100 | Flush when this many events are buffered. |
max_batch_size | 500 | Events per batch request. 500 is the maximum. |
max_retries | 3 | Retries per batch in one flush. |
request_timeout | 10.0 | Per-request timeout (seconds). |
disabled | false | Captures and sends nothing. Useful in tests. |
debug | false | Logs warnings with error_log. |
on_error | — | callable(WhisperrError): void. Types: auth, dropped, retry_exhausted. |
Common questions
Does the PHP SDK slow down my Laravel responses?
No. In Laravel the SDK buffers events during the request and sends them in the app's terminating hook, after the response is sent.
How do I stop a retried webhook from recording the same event twice?
Pass a stable messageId built from your own data, and the original occurredAt, to track(). Whisperr keeps one event per message ID for each app.
Which PHP and Laravel versions does whisperr/php support?
PHP 8.0 or later with ext-curl and ext-json. Laravel is optional; CI tests Illuminate 8.83 on PHP 8.0 and Illuminate 10 and 11 on PHP 8.1 to 8.3.
Which API key does the PHP SDK use?
The server key (wrk_…), shown as a Server key in Settings → API keys. Keep it in your environment, never in front-end code.