notifypress / sdk
Official PHP SDK for the NotifyPress REST API — send and manage web push notifications, subscribers, segments, and automations.
Requires
- php: ^8.1
- ext-json: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- guzzlehttp/guzzle: ^7.9
- pestphp/pest: ^2.34 || ^3.5 || ^4.0
- phpstan/phpstan: ^2.0
Suggests
- ext-curl: Enables the default zero-dependency cURL transport (falls back to PHP streams).
- guzzlehttp/guzzle: Detected automatically and used as the HTTP client when installed (e.g. in Laravel).
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-01 21:15:33 UTC
README
Official PHP client for the NotifyPress REST API — browser push notifications, subscribers, segments, and automation workflows.
Runs unchanged in WordPress, Laravel, and plain PHP, with no required runtime dependencies.
use NotifyPress\NotifyPress; use NotifyPress\Payload\SendNotification; $client = new NotifyPress(getenv('NOTIFYPRESS_API_TOKEN')); $client->notifications->send( SendNotification::make('Flash sale', '25% off, today only') ->targetUrl('https://example.com/sale') );
Requirements
- PHP 8.1+
ext-json, plus eitherext-curlorallow_url_fopen(any PSR-18 client works too)
Installation
composer require notifypress/sdk
Credentials
NotifyPress issues two credentials per site, and they are not interchangeable:
| Where to find it | Where it may be used | |
|---|---|---|
| API token | Site settings → API token | Servers only. It is a secret with full access to the site. |
Public key (npk_...) |
Site settings → widget snippet | Safe to embed in a page. Used only by the public opt-in endpoints. |
Use NotifyPress with the API token, and NotifyPressPublic with the public key.
Pass the token exactly as the dashboard shows it. If you have a value containing a pipe, use only the part after the pipe. The part before it is an internal id the server supplies itself; including it still authenticates, so nothing looks wrong, but the site can no longer be resolved and every call fails with
NotFoundException: No site is linked to this API token.
Quickstart per environment
Plain PHP
Nothing to configure. The SDK falls back to cURL, then PHP streams.
require 'vendor/autoload.php'; $client = new NotifyPress\NotifyPress('your-api-token'); foreach ($client->subscribers->list() as $subscriber) { echo $subscriber->token, PHP_EOL; }
Laravel
Guzzle is already installed, so the SDK detects it and reuses the application's HTTP stack. Bind it once:
// app/Providers/AppServiceProvider.php use NotifyPress\NotifyPress; $this->app->singleton(NotifyPress::class, fn () => new NotifyPress(config('services.notifypress.token')));
public function send(NotifyPress $notifypress): RedirectResponse { $notifypress->notifications->send( SendNotification::make($request->title, $request->message) ); return back(); }
WordPress
Calls are routed through wp_remote_request() automatically, so the site's proxy constants, http_request_args filters, and WP_HTTP_BLOCK_EXTERNAL allowlist are all honoured — a raw cURL call from a plugin bypasses every one of them and breaks on managed hosts requiring an outbound proxy.
require_once __DIR__ . '/vendor/autoload.php'; $client = new NotifyPress\NotifyPress(get_option('notifypress_api_token')); $site = $client->sites->current();
Anything else
Pass your own PSR-18 client:
use NotifyPress\Config; use NotifyPress\Http\Client\Psr18Client; $client = new NotifyPress( Config::make('your-api-token')->withHttpClient(new Psr18Client($yourPsr18Client)) );
Configuration
Config is immutable — every with*() returns a new instance:
use NotifyPress\Config; $client = new NotifyPress( Config::make('your-api-token') ->withBaseUrl('https://app.notifypress.com/api/v1') ->withTimeout(10.0) ->withMaxRetries(4) ->withHeader('X-Request-Source', 'my-app') );
A built client can be re-pointed without rebuilding it: $client->withBaseUrl(...), ->withApiToken(...), ->withTimeout(...).
Resources
sites, notifications, subscribers, tags, segments, automations, reports, announcements, dashboard.
$client->sites->current(); $client->sites->updateCurrent(SiteUpdate::make('Example', 'https://example.com')->promptDelaySeconds(5)); $client->notifications->list(NotificationQuery::make()->status(NotificationStatus::SENT)->limit(50)); $client->notifications->retrieve(9); $client->notifications->cancel(9); $client->subscribers->create(SubscriberCreate::make($fcmToken)->country('CA')->browser('Chrome')); $client->subscribers->addTag($fcmToken, $tagId); $client->subscribers->delete($subscriberId); // numeric ID, not the token $client->tags->create(TagCreate::interest('Deals')); $client->segments->previewCount(SegmentRule::make()->location(['CA', 'US'])); $client->automations->trigger('cart_abandoned', $fcmToken); $client->dashboard->analytics();
Fluent payloads
Every payload and query is an immutable builder, so a base can be safely specialised:
$base = SendNotification::make('Flash sale', '25% off'); $client->notifications->send($base->toSegment(4)); $client->notifications->send($base->toEveryone()->iconUrl('https://example.com/icon.png'));
Plain arrays work everywhere a builder does, and ->set('key', $value) passes through any parameter the SDK does not model yet:
$client->notifications->send(['title' => 'Flash sale', 'message' => '25% off']); $client->notifications->send($base->set('ttl_seconds', 300));
Pagination
Iterating a list yields the current page. Walking the whole set is explicit, because every step is another HTTP round trip:
$page = $client->subscribers->list(SubscriberQuery::make()->limit(100)); $page->total; // cached count of the full set $page->hasNextPage(); $page->nextPage(); foreach ($page as $subscriber) { } // this page foreach ($page->autoPaging() as $subscriber) { } // every page, fetched lazily
GET /automations is offset-paginated and returns an OffsetPage with currentPage, lastPage, and isPaidUser().
Errors
Every non-2xx response throws. The three server error shapes are normalised into one hierarchy:
NotifyPressException
├── ApiException {status, body, response}
│ ├── ValidationException {errors} 422
│ ├── AuthenticationException 401, or 403 rejecting the credential
│ ├── PaidPlanException {upgradeUrl} 403 on a paid-plan gate
│ ├── ForbiddenException other 403
│ ├── NotFoundException 404
│ ├── RateLimitException {retryAfter} 429
│ └── ServerException 5xx
├── TimeoutException
├── ConnectionException
└── ConfigurationException
use NotifyPress\Exception\PaidPlanException; use NotifyPress\Exception\RateLimitException; use NotifyPress\Exception\ValidationException; try { $client->notifications->send(SendNotification::make($title, $message)); } catch (ValidationException $e) { echo $e->first('title'); // "The title may not be greater than 50 characters." } catch (PaidPlanException $e) { echo $e->upgradeUrl; } catch (RateLimitException $e) { sleep($e->retryAfter ?? 5); }
Credentials never reach a message or a stack trace — see NotifyPress\Http\Redactor, which is what everything diagnostic goes through.
Retries
Idempotent requests are retried up to maxRetries times (default 2) on 429/502/503/504 and on connection failures, honouring Retry-After up to a 10-second cap, otherwise exponential backoff with jitter.
Mutations are never retried automatically. A send that times out may already have gone out, and retrying it would push a duplicate to the entire audience. Opt in per call when a request really is safe to repeat:
use NotifyPress\Http\RequestOptions; $client->tags->delete($tagId, RequestOptions::make()->retryable());
Scheduling and timezones
scheduled_at is Y-m-d H:i:s in the account's display timezone — not ISO 8601, and not UTC. The SDK formats the wall-clock time exactly as given and performs no conversion, so pass a DateTime already expressed in that timezone:
$client->notifications->schedule( ScheduleNotification::make('Morning', 'Rise and shine') ->at(new DateTimeImmutable('2026-08-15 09:30:00')) ); RecurringSchedule::weekly('09:30')->onDays([1, 3, 5]); // Mon/Wed/Fri RecurringSchedule::monthly('09:30')->onDayOfMonth(15);
Datetimes on responses stay strings for the same reason — parsing them here would attach the wrong timezone.
Uploads
MultipartBody builds the request bytes by hand, so one code path works across every transport:
use NotifyPress\Http\Multipart\MultipartBody; $client->notifications->create( MultipartBody::make() ->field('title', 'Flash sale') ->field('message', '25% off') ->file('icon', '/path/to/icon.png') // max 2 MB ->file('image', '/path/to/hero.jpg') // max 4 MB );
Oversized or wrongly-typed files fail locally with the same field key the API would have used, so there is one error path either way. ->withoutClientValidation() defers entirely to the server.
Public endpoints
For visitor opt-in, keyed by the public npk_ key:
use NotifyPress\NotifyPressPublic; use NotifyPress\Payload\PublicSubscribe; $push = new NotifyPressPublic('npk_...', 'https://example.com'); $push->subscribe(PublicSubscribe::make($fcmToken)->browser('Chrome')->device('Desktop')); $push->unsubscribe($fcmToken); // keeps the row, marks it inactive $push->erase($fcmToken); // GDPR hard delete, 5/min $push->recentNotifications();
The site URL is required. The server validates Origin/Referer against the site registered in NotifyPress (exact host or the www. variant) and rejects requests carrying neither. A browser sets that header itself; server-side PHP does not, so the SDK sends it explicitly. Give the URL registered in NotifyPress, or every call will 403.
Forward compatibility
The SDK is built not to break when the API grows:
- Models keep the full decoded payload. A new field is readable through
$model->get('new.field')instead of being dropped. - Enums are read with
tryFrom(). A status this release has never heard of leavesstatusEnum()null while$model->statusstill holds the raw string — it never fatals. $client->transport()reaches any endpoint the SDK does not model yet, with auth, retries, and error mapping intact.
Development
composer install composer test # Pest, no network composer phpstan # level 8, analysed at the 8.1 floor composer cs # PSR-12 composer check # all three
License
MIT