Drop-in Laravel package that pings you on Slack, Discord, and/or Twilio SMS whenever someone subscribes or purchases on your platform.
Works with Cashier, Spark, Paddle, custom billing — anything. You fire an event (or call a facade); Cha-Ching handles delivery.
Coding agents: start with AGENTS.md (full integration guide) and llms.txt (capability index). Do not reverse-engineer APIs from source when those docs cover the task.
- PHP 8.2+
- Laravel 11, 12, or 13
composer require boone-studios/cha-ching
php artisan vendor:publish --tag=cha-ching-configAuto-discovery registers the service provider and ChaChing facade.
Set the channels you want. Any channel with credentials will be auto-enabled; you can also set channels explicitly in config/cha-ching.php.
CHA_CHING_ENABLED=true
# Slack incoming webhook
CHA_CHING_SLACK_WEBHOOK=https://hooks.slack.com/services/...
# Discord webhook
CHA_CHING_DISCORD_WEBHOOK=https://discord.com/api/webhooks/...
# Twilio SMS (you/your team — not the customer)
CHA_CHING_TWILIO_SID=ACxxxxxxxx
CHA_CHING_TWILIO_TOKEN=your-auth-token
CHA_CHING_TWILIO_FROM=+15551234567
CHA_CHING_TWILIO_TO=+15557654321,+15559876543
# Queue (default: true — run a queue worker in production)
CHA_CHING_QUEUE=true| Variable | Purpose |
|---|---|
CHA_CHING_ENABLED |
Master switch (false = no-op) |
CHA_CHING_SLACK_WEBHOOK |
Slack incoming webhook URL |
CHA_CHING_DISCORD_WEBHOOK |
Discord channel webhook URL |
CHA_CHING_TWILIO_* |
Twilio credentials + recipient list |
CHA_CHING_QUEUE |
Queue the listener (recommended in production) |
CHA_CHING_CASHIER_ENABLED |
Auto-listen to Cashier Stripe webhooks when Cashier is installed (default true) |
CHA_CHING_CASHIER_RENEWALS |
Also alert on subscription renewals (default false) |
CHA_CHING_CUSTOMER_URL |
Admin URL template with {id} for the billable model |
use BooneStudios\ChaChing\AlertType;
use BooneStudios\ChaChing\Data\RevenueAlert;
use BooneStudios\ChaChing\Facades\ChaChing;
ChaChing::subscription(new RevenueAlert(
type: AlertType::Subscription,
customerName: $user->name,
customerEmail: $user->email,
productName: 'Pro',
amountInCents: 2900,
currency: 'USD',
interval: 'month',
source: 'stripe',
url: route('admin.users.show', $user),
metadata: ['plan_id' => 'price_pro_monthly'],
));
ChaChing::purchase(new RevenueAlert(
type: AlertType::Purchase,
customerName: $user->name,
customerEmail: $user->email,
productName: 'Lifetime Pack',
amountInCents: 9900,
source: 'paddle',
));
// Or route by the alert's type:
ChaChing::alert($alert);use BooneStudios\ChaChing\Events\SubscriptionCreated;
use BooneStudios\ChaChing\Events\PurchaseCompleted;
event(new SubscriptionCreated($alert));
event(new PurchaseCompleted($alert));Both paths share the same listener and channels.
If laravel/cashier is installed, Cha-Ching automatically listens to Cashier's WebhookHandled event and maps Stripe webhooks into revenue alerts. No Cashier dependency is required for the package itself — the integration only boots when Cashier is present.
| Stripe event | Alert | Notes |
|---|---|---|
customer.subscription.created |
Subscription | New subs (Checkout or API) |
checkout.session.completed |
Purchase | Only mode=payment + paid (subscription mode skipped to avoid dupes) |
invoice.payment_succeeded (manual) |
Purchase | One-off invoices / charges |
invoice.payment_succeeded (subscription_cycle) |
Subscription (renewal) | Off by default |
invoice.payment_succeeded (subscription_create) |
— | Skipped (covered by subscription.created) |
- Install Cashier and point Stripe webhooks at Cashier as usual (
/stripe/webhook). - Require Cha-Ching — auto-discovery is enough.
- Optional env:
CHA_CHING_CASHIER_ENABLED=true
CHA_CHING_CASHIER_RENEWALS=false
# Deep link to the billable record in your admin ({id} = local primary key)
CHA_CHING_CUSTOMER_URL="${APP_URL}/nova/resources/users/{id}"When a webhook includes a Stripe customer id, Cha-Ching resolves the billable model via config('cashier.model') (fallback App\Models\User) using the stripe_id column, and prefers that model's name / email on the alert.
CHA_CHING_CASHIER_ENABLED=falseYou can still call ChaChing::subscription() / ::purchase() manually from your own listeners.
use BooneStudios\ChaChing\AlertType;
use BooneStudios\ChaChing\Data\RevenueAlert;
use BooneStudios\ChaChing\Facades\ChaChing;
ChaChing::subscription(new RevenueAlert(
type: AlertType::Subscription,
customerName: $user->name,
customerEmail: $user->email,
productName: 'Pro',
amountInCents: 2900,
source: 'stripe',
));Slack / Discord
💰 New subscription
Customer: Alice Smith <alice@example.com>
Plan: Pro · $29.00/month
Source: stripe
Link: https://app.example.com/admin/users/1
SMS
Cha-Ching: Alice Smith subscribed to Pro — $29.00/month
The SendRevenueAlert listener is queueable so checkout requests are not blocked by Slack/Discord/Twilio latency.
CHA_CHING_QUEUE=true(default) — uses your app queue connection (run a worker in production).CHA_CHING_QUEUE=false— forces thesyncconnection so alerts still send without a worker.
Optional overrides:
CHA_CHING_QUEUE_CONNECTION=redis
CHA_CHING_QUEUE_NAME=notificationsWhen you wire a new project, send a fake alert without a real sale:
php artisan cha-ching:test
php artisan cha-ching:test --type=subscription
php artisan cha-ching:test --channel=slackThe command forces sync delivery so you do not need a queue worker just to verify credentials.
Chat/SMS openers pick a random “you got paid” line from the app locale (config('app.locale')).
Ship with en, es, fr, and de. Publish and customize:
php artisan vendor:publish --tag=cha-ching-langEdit lang/vendor/cha-ching/{locale}/messages.php — especially the celebrations and celebrations_sms arrays. Placeholders: :amount, :customer, :product.
CHA_CHING_RANDOM_CELEBRATIONS=trueMessages are prefixed with the host app name so you can tell projects apart:
💰 Canderbox: You got paid!
Defaults to config('app.name'). Override or disable:
CHA_CHING_APP_NAME=Canderbox
CHA_CHING_SHOW_APP_NAME=trueStripe retries. Cashier auto-listeners claim each Stripe event id (evt_…) in cache for 24h so the same event does not double-ping you.
CHA_CHING_DEDUPE=true
CHA_CHING_DEDUPE_TTL=86400
# CHA_CHING_DEDUPE_STORE=redis # optional cache storeUse a shared cache (Redis) in production so multiple app servers share the claim.
- Operator alerts only — messages go to your Slack/Discord/phones, not the customer.
- Missing credentials — that channel is skipped with a warning; others still send.
- Channel failures — errors are logged per channel and do not block the rest.
- No channels configured — silent no-op (debug log).
- Metadata on chat messages — off by default (
CHA_CHING_INCLUDE_METADATA); Stripe ids stay in structured logs/metadata only.
Tests are written with Pest.
composer install
composer test
# or
./vendor/bin/pestMIT