Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Notification Mastodon Laravel Package

revolution/laravel-notification-mastodon

A Laravel notification channel for sending notifications to Mastodon. Publish status updates directly from your app using Laravel's notification system, with simple configuration and support for the Mastodon API so you can integrate posting into existing workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require revolution/laravel-notification-mastodon

The package is a thin Laravel notification channel — there is nothing to publish in most versions. Configure credentials in config/services.php:

'mastodon' => [
    'domain' => env('MASTODON_DOMAIN'), // e.g. https://mastodon.social
    'token'  => env('MASTODON_TOKEN'),
],

Get a token before writing any code. On your target instance go to Preferences → Development → New application, grant at minimum write:statuses (add write:media if you attach images, read:accounts if you want to resolve handles). Copy the access token into .env. A token without write:statuses will fail with 401/403 at send time — verify this first, it is the single most common reason the first send fails.

First use case — one account, one status

php artisan make:notification PostToMastodon
use Revolution\Notification\Mastodon\MastodonChannel;
use Revolution\Notification\Mastodon\MastodonMessage;

public function via($notifiable): array
{
    return [MastodonChannel::class];
}

public function toMastodon($notifiable): MastodonMessage
{
    return MastodonMessage::create('Deployed at '.now()->toDateTimeString());
}

Trigger it: Notification::route('mastodon', config('services.mastodon.token'))->notify(new PostToMastodon);

Where to look first

  • vendor/revolution/laravel-notification-mastodon/src/ — read MastodonChannel.php and MastodonMessage.php before anything else. With ~6 stars and no dependents, the README is not a reliable contract; the source is. Confirmed the autoload namespace there (likely Revolution\Notification\Mastodon) since the fork's namespace may differ from the upstream NotificationChannels\Mastodon examples you'll find on the web.
  • Check the package's composer.json for the supported Laravel/PHP range before assuming it works with your version — a low-adoption fork may lag behind current Laravel.
  • The channel resolves the HTTP endpoint from services.mastodon.domain; confirm whether it appends /api/v1/statuses itself or expects a full base URL.

Implementation Patterns

Per-notifiable routing (multi-account)

Rather than a single global account, let the notifiable own its token:

// App\Models\User.php
public function routeNotificationForMastodon(): ?string
{
    return $this->mastodon_token; // encrypted cast recommended
}

Keep the global services.mastodon entry as a fallback/default. This is the pattern that scales — a single deployment posting to several accounts, or a SaaS where each tenant has its own instance.

On-demand for arbitrary instances

Notification::route('mastodon', $token)
    ->notify(new PostToMastodon($status));

If the package reads the domain only from config (not from the route), on-demand against a different instance will not work. Check MastodonChannel::send() — if it doesn't accept a [token, domain] pair, add a small custom channel class of your own that wraps the package's message object and posts to a per-call endpoint. The message class is still reusable.

Visibility and content shaping

return MastodonMessage::create($text)
    ->unlisted()          // unlisted | private | direct
    ->spoilerText('Spoilers below')
    ->language('en');

Use direct() for DMs (and remember a direct status with no mentioned accounts is effectively unaddressed — Mastodon requires @handle inside the body for DMs).

Media attachments

return MastodonMessage::create('New chart')
    ->withMedia([Storage::path('charts/today.png')]);

Mastodon's media upload is two-step (POST /api/v1/media, then attach media_ids to the status). Uploads may return 202 while processing; if the package doesn't poll, image posts will intermittently 422. If you see that, attach only small/ready files or wrap the send in a retry.

Queue the notification, not the channel

class PostToMastodon extends Notification implements ShouldQueue
{
    public function via($notifiable): array { return [MastodonChannel::class]; }
}

Mastodon API calls are slow (100–600ms plus media processing) and rate-limited. Never send synchronously in a request cycle. Add ->onQueue('social') and a tries/backoff strategy.

Testing

Notification::fake();
$user->notify(new PostToMastodon);
Notification::assertSentTo($user, PostToMastodon::class);

For the channel itself, Http::fake(['*/api/v1/statuses' => Http::response(['id' => '1'], 200)]) and assert the request body — this is how you catch visibility/spoiler regressions.

Integration tip: pair it with a "social outbox" table (status, target, external id, sent_at) so you get idempotency and an audit trail the package doesn't provide. Store the returned status id/url — users will ask "did it actually post?"

Gotchas and Tips

  • Token scopes are the #1 failure. write:statuses is required; media needs write:media. A missing scope surfaces as 401 Unauthorized from the API, not a clear "scope" error. Verify scopes on the instance before debugging code.
  • Character limits are instance-specific and count differently. Default is 500, but instances override it and URLs always count as 23 characters regardless of length. Don't hardcode 500; truncate defensively or check /api/v1/instance for max_toot_chars.
  • Rate limits: ~300 requests / 5 minutes per account. The package does not handle 429 or Retry-After. Rely on queues plus Laravel's backoff(), and consider RateLimited middleware if you fan out to many accounts.
  • Domain format matters. mastodon.social vs https://mastodon.social — check whether the channel concatenates /api/... or uses the domain verbatim. A duplicated or missing scheme yields Could not resolve host or a 404 that looks like an auth problem.
  • Idempotency is not built in. A retried queued notification posts the status twice, since Mastodon has no idempotency key. Guard with your own outbox row or a ShouldBeUnique job.
  • Debugging: log Http::withOptions(['debug' => true]) or use Http::fake() to dump the outgoing payload. Laravel's Http client exceptions omit the response body by default; wrap the send in a try/catch on RequestException and log $e->response->body() — Mastodon returns useful error strings there.
  • Low-adoption risk. 6 stars and 0 dependents means no community test coverage. Pin the version in composer.json, read the source before upgrading Laravel, and be ready to vendor it. The surface area is small enough that a fork or a ~100-line custom channel is a legitimate exit strategy if the package stalls.
  • Extension points: the MastodonMessage value object is the useful part — even if you replace the channel, keep returning MastodonMessage from toMastodon() so notifications stay portable. If you need idempotency, scheduling, or per-instance routing the package lacks, subclass MastodonChannel and override send() rather than rewriting notifications.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin