zapmizer/laravel-notification-zapmizer
A Laravel notification channel for Zapmizer that lets you send notifications through Zapmizer's API using Laravel's built-in notification system. Configure credentials, route recipients, and deliver messages cleanly from your application.
Install the package and publish its config, then wire credentials into .env:
composer require zapmizer/connect
php artisan vendor:publish --tag="zapmizer-config"
ZAPMIZER_BASE_URL=https://...
ZAPMIZER_TOKEN=...
Look first at config/zapmizer.php (base URL, timeout, retry knobs) and at the two classes you will touch daily: InstanceClient (single-shot API calls such as media()) and ZapmizerConnection (higher-level flows such as awaitMedia() and syncMessages()).
First use case — fetch and inspect media:
use Zapmizer\Connect\InstanceClient;
try {
$media = app(InstanceClient::class)->media($mediaId);
} catch (\Zapmizer\Connect\Exceptions\MediaRejectedException $e) {
report($e); // $e->reason() explains the 422
} catch (\Zapmizer\Connect\Exceptions\MediaRateLimitedException $e) {
// $e->retryAfter() — seconds, or null if the header was absent
}
If you were relying on raw HTTP status codes from media(), note that as of 0.3.1 a 422 and a 429 no longer surface as generic connection errors — they arrive as the two dedicated exception types above.
For message ingestion, locate syncMessages() next. Its contract is cursor-based: it returns messages saved after the cursor, not a general change feed. Release 0.4.1 is a documentation-only correction to the 0.4.0 wording around this method.
Let awaitMedia() absorb rate limits, not rejections. ZapmizerConnection::awaitMedia() now treats a 429 as a transient downloading state and sleeps for the server-provided Retry-After before polling again. A 422 is not absorbed — it propagates immediately, so a bad/expired media reference fails fast instead of burning your timeout budget.
use Zapmizer\Connect\ZapmizerConnection;
use Zapmizer\Connect\Exceptions\MediaRejectedException;
try {
$media = app(ZapmizerConnection::class)->awaitMedia($mediaId, timeout: 120);
} catch (MediaRejectedException $e) {
// Unrecoverable: don't retry, surface reason() to the user or mark the record failed
$this->fail($e->reason());
}
Catch children before the parent. Both new exceptions extend ZapmizerConnectException. Order your catch blocks specific → general, or a broad catch (ZapmizerConnectException $e) will swallow the rate-limit signal you wanted to act on.
Drive queue backoff from the exception. In a job, let retryAfter() decide the delay rather than a fixed constant:
public function handle(InstanceClient $client): void
{
try {
$client->media($this->mediaId);
} catch (MediaRateLimitedException $e) {
$this->release($e->retryAfter() ?? 30);
} catch (MediaRejectedException $e) {
$this->delete(); // permanent failure — retrying won't help
}
}
Keep $tries modest and use retryAfter() as the floor; pair with backoff() on the job for the non-429 cases.
Separate "not ready yet" from "never going to be ready." For long-running media (video processing, large uploads), prefer awaitMedia() for the polling, and reserve direct media() calls for short-lived, user-triggered lookups where you want immediate 429 feedback in the request cycle.
Treat syncMessages() as an append-only cursor feed. It returns messages saved after the cursor. Messages that arrive during a reconnection sync are included, but an edit to a message that already existed will not be returned — Zapmizer paginates by primary key. Persist the cursor per account/connection and upsert by provider ID so reconnection syncs stay idempotent.
$cursor = $connection->zapmizer_cursor;
$messages = app(ZapmizerConnection::class)->syncMessages($cursor);
foreach ($messages as $message) {
InboxMessage::updateOrCreate(
['zapmizer_id' => $message->id],
['payload' => $message->toArray()]
);
// Use the provider's cursor/primary key returned by the API.
$cursor = max($cursor, $message->id);
}
$connection->update(['zapmizer_cursor' => $cursor]);
Do not use syncMessages() to propagate edits or deletions. For those, use webhooks, a separate reconciliation job, or a periodic full sync that does not rely solely on the saved-after-cursor feed.
Map reason() into your domain. Log the raw reason() and translate it to a friendly message at the boundary; do not leak provider phrasing to end users.
ZapmizerConnectException to detect a 422 will silently change behaviour if it has not been audited since 0.3.1. Grep for media( and getStatusCode() before bumping.syncMessages() is not a change-data-capture feed. The 0.4.1 docs correct the 0.4.0 wording: it returns messages saved after the cursor, including messages that arrive during a reconnection sync, but it does not return edits to existing messages. Zapmizer paginates by primary key, so an updated row will not reappear just because its content changed.updateOrCreate() keyed by the provider message ID, you will create duplicates. Without persisting the highest cursor returned, you will either re-read or skip messages.retryAfter() can be null. If the upstream response omits Retry-After, pick your own default (30–60s) rather than releasing immediately and hammering the endpoint.awaitMedia() is by design. Wrap the call, not the loop, and treat it as terminal — retry policies that blanket-retry all ZapmizerConnectException will loop forever on a rejected asset.sleep()/usleep() poller before 0.3.1, replace it; the built-in path is now rate-limit aware and respects server timing.Http::fake()) and assert the typed exceptions are thrown for 422/429; assert awaitMedia() issues a second request after the Retry-After window rather than asserting on wall-clock time (Carbon::setTestNow() / travel()). For syncMessages(), test cursor persistence, duplicate-free reconnection syncs, and that an edited existing message is not expected to reappear.ZapmizerConnectException is still the right place for global reporting/logging in bootstrap/app.php or your handler — just make sure the specific types are handled first in caller code.How can I help you explore Laravel packages today?