Skip to content

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.

composer require whisperr/php

The 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.

Package discovery registers the service provider and the Whisperr facade. Set the server key in .env:

.env
WHISPERR_API_KEY=wrk_your_server_key
WHISPERR_APP_VERSION=2026.10.1 # optional: your backend release

To edit the config file, publish it:

php artisan vendor:publish --tag=whisperr-config
use 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):

KeyEnvironment variableDefault
api_keyWHISPERR_API_KEY""
base_urlWHISPERR_BASE_URLhttps://api.whisperr.net
app_versionWHISPERR_APP_VERSION—
disabledWHISPERR_DISABLEDfalse
debugWHISPERR_DEBUGfalse

Create the server key as a Server key in Settings → API keys.

whisperr.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 shutdown

track() 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.

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.

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.
  • occurredAt must be RFC 3339 with a time zone (Z or +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']]);
context keyValue
sdk_namewhisperr-php
sdk_versionWhisperr::VERSION, for example 0.3.0
app_versionThe app_version option. Empty: no key.

A key you pass in the $context argument of track() wins over these.

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() throws InvalidArgumentException when currency is not three letters, or price is 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_RECOVERED and REFUND_GRANTED (BillingEvent::ALL lists 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.

  • Events buffer in memory and go out in batches on flush(): after the Laravel response, when flush_at events are buffered, or at shutdown.
  • Events go to POST /v1/events/batch and identify calls to POST /v1/identify, in order, with the X-API-Key header.
  • 429, 5xx and network errors are retried up to max_retries times. The wait between tries is at most about 2 seconds, so request teardown does not hang. The PHP SDK does not read Retry-After.
  • On 401, 403 or exhausted retries, the batch stays buffered for the next flush() in the same process. Other 4xx responses drop the batch and report dropped.
  • The buffer lives only as long as the PHP process or request.

See Delivery, retries, idempotency.

KeyDefaultNotes
api_key—Server key (wrk_…). Required unless disabled.
base_urlhttps://api.whisperr.netIngestion base URL.
app_version—Your backend release. Sent as context.app_version.
flush_at100Flush when this many events are buffered.
max_batch_size500Events per batch request. 500 is the maximum.
max_retries3Retries per batch in one flush.
request_timeout10.0Per-request timeout (seconds).
disabledfalseCaptures and sends nothing. Useful in tests.
debugfalseLogs 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.