balazstanka/laravel-telegram-alerts
Laravel package for sending alerts and notifications to Telegram chats via the bot API, with configurable recipients and simple integration for error monitoring, system alerts, and custom messages.
This package wires Laravel into the Telegram Bot API so you can push operational alerts (deploys, failed jobs, exceptions, form submissions) to a chat, group, or channel.
composer require balazstanka/laravel-telegram-alerts
php artisan vendor:publish --tag="telegram-alerts-config"
.env:
TELEGRAM_BOT_TOKEN=123456:ABC...
TELEGRAM_CHAT_ID=-1001234567890
getUpdates/sendMessage will silently return {"ok":false,"description":"chat not found"} otherwise.config/telegram-alerts.php (token/chat id keys, and any default parse mode or queue settings), then the README section referenced by v1.1.0 for addUrl().First use case: a deploy or failed-job alert. Start with the simplest send, then add a link button so recipients can jump straight to the CI run, the failing job in Horizon, or the PR:
// v1.1.0+
TelegramAlert::message('Deploy to production finished ✅')
->addUrl('Open release', 'https://github.com/org/repo/releases/tag/v1.1.0')
->addUrl('View logs', 'https://example.com/logs/deploy-123')
->send();
The addUrl($text, $url) addition is the headline feature of v1.1.0 — buttons render as an inline keyboard beneath the message, so the recipient stays in Telegram and taps straight through instead of copy-pasting a URL out of message text.
Dedicated alert classes. Rather than scattering send calls, wrap each alert in a small class (or a method on an existing service) so the message text, chat target, and buttons live in one testable place:
class DeployFailedAlert
{
public function __construct(public Deployment $deployment) {}
public function send(): void
{
TelegramAlert::message("❌ Deploy failed: {$this->deployment->branch}")
->addUrl('Pipeline', route('pipelines.show', $this->deployment))
->addUrl('Commit', $this->deployment->commitUrl)
->send();
}
}
Link buttons are the main workflow win. Good button candidates are the ones people currently have to hunt for: the failing queue job, the Sentry issue, the diff, the admin record. Put one primary action in the first row and secondary actions after it; Telegram lays buttons out left-to-right in the order you add them, so call addUrl() in priority order. Two addUrl() calls is usually the sweet spot; five is noise on mobile.
Hook into Laravel's built-in failure surfaces. The highest-value integrations are Queue\Events\JobFailed, Illuminate\Foundation\Events\DiagnosingHealth, the exception handler's report()/render() for production-only alerts, and Schedule::onFailure(). Keep alerts out of the happy path of HTTP requests — see the queuing note below.
Queue the send. Telegram calls are outbound HTTP; do not do them in the request cycle of a user-facing action. Either dispatch the alert class as a queued job or wrap the call in dispatch(fn () => ...) after the response (dispatchAfterResponse). If the package exposes a queueable alert/job, use it and set a dedicated queue so a Telegram outage never backs up real work.
Environment-aware routing. Point TELEGRAM_CHAT_ID at a private dev chat in local/staging and the ops channel in production — either via per-environment .env files or by overriding the config value in a service provider. Nothing is worse than a staging deploy alert paging the on-call channel at 3am.
Testing. Fake the HTTP layer and assert on the outgoing payload, so you check both the text and the reply_markup structure rather than guessing:
Http::fake();
(new DeployFailedAlert($deployment))->send();
Http::assertSent(fn ($request) =>
str_contains($request['text'], 'Deploy failed')
&& $request['reply_markup']['inline_keyboard'][0][0]['url'] === $deployment->commitUrl
);
This is also the cheapest way to pin down the exact JSON shape addUrl() produces for your version.
-100…); a bare username (@mychannel) also works for public channels. If alerts vanish with no error in your logs, chat not found or bot was blocked by the user is almost always the cause — log the decoded response body, not just the status code.{"ok": false, "description": "..."} for most errors. Make sure your error handling inspects the ok field, or you will ship silently broken alerting.addUrl() accepts URLs only. Inline keyboard URL buttons require an http:// or https:// URL (with tg:// for Telegram deep links). Localhost, relative paths, and long query strings with unencoded characters will be rejected or silently fail; run route() output through url()->to() rather than passing a raw path.callback_data support and therefore no way to handle a button press back in your app — don't design an "Approve / Reject" flow on top of this yet. If you need interactivity, that's the extension point to watch (and it requires a webhook endpoint plus setWebhook).429 with retry_after) and then muted. Always aggregate: one alert per incident, or a "5 jobs failed in the last minute" summary, plus a cooldown/dedupe cache key.Str::limit) before sending, or the API rejects the whole message._, *, [, or < in a branch name, exception message, or URL breaks the whole message. Prefer plain text for alert bodies with URLs in buttons, or escape rigorously.config:cache will bite you. Token and chat ID are read from config; after changing .env in a deployed environment, re-run php artisan config:cache. Alerts continuing to go to the old chat after a config change is this, not a package bug.try/catch, log at warning level, and never let alerting exceptions escape into the queue worker's failure handler (which can cause recursive alerts).addUrl() is additive — no breaking changes reported — but confirm your installed constraint (^1.1) and re-run your fake-HTTP assertions, since the reply_markup payload only appears from this version onward.How can I help you explore Laravel packages today?