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

Captainhook Laravel Package

mpociot/captainhook

CaptainHook makes it easy to add webhooks to your Laravel application: map Eloquent model events such as created, updated, and deleted to outgoing HTTP requests, so external services are notified automatically whenever your data changes.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm what you're installing before you plan anything. The repo tagline and the package description agree — CaptainHook is for outgoing webhooks: you map Laravel events to subscriber URLs, the package serializes a JSON payload, queues the POST, and stores a delivery record. (One older assessment in the supplied material reads it as a .git/hooks manager; that's wrong for this package and a good reminder to trust src/ over any README.) Open composer.json, src/, and the service provider once before estimating.

Minimal setup:

composer require mpociot/captainhook          # pin it — last release Dec 2016
php artisan vendor:publish --provider="Mpociot\CaptainHook\CaptainHookServiceProvider"
php artisan migrate                          # webhook table + delivery log table

Then read, in order:

  1. config/captainhook.php — the event → handler map. This file is your day-to-day surface.
  2. src/ — specifically the webhook Eloquent model, the queue job that performs the HTTP call, and the listener/subscriber that wires Laravel events to that job. Class names in the 2016 README won't match reality; the source will.
  3. The published migration — the columns tell you what the package already logs (URL, event, payload, response code/body, success flag, timestamps, active toggle). That schema is what your debugging and admin UI will sit on.

First use case: attach the package's model trait / event binding to one model, subscribe webhook.site's URL to a single created event, fire it through a real queue (not sync), and watch the POST arrive. Only then widen to updated, deleted, and any custom domain events. Prove the full loop — dispatch → queued job → HTTP → stored response — before you write any payload-shaping code, because the queue and serialization are where this package will fight you.

What you're actually buying: you are skipping the boilerplate of "listen to event → build payload → queue HTTP POST → persist result." You are not buying delivery guarantees, signing, retries, or a subscriber UI. Treat the install as a starting scaffold, not a platform.

Implementation Patterns

Everything asynchronous, always. The core pattern is: Laravel event fires → listener resolves matching active webhooks → each dispatches a queued job → job POSTs the JSON payload → job writes the response back to the delivery record. Keep every webhook on a queued connection (database or redis). A sync driver turn this into an outbound HTTP call inside your request cycle — one slow customer endpoint and your checkout page times out. Set explicit queue names so webhook jobs can be retried and scaled independently of mail or other jobs.

Model trait + explicit event map. Bind webhooks to models via the package trait and register the events you care about in config. Keep the event naming boring and stable (order.created, invoice.paid) because those strings become a public contract the moment a customer subscribes. Resist exposing every Eloquent event by default — updated fires constantly and will hammer subscribers.

Shape the payload yourself; never toArray() the model. Build an explicit array of whitelisted fields in a small transformer class per event type. This does three jobs: it prevents accidental leakage of internal columns, it gives you a stable schema to version (payload_version), and it lets you keep the transformer in your app where you can unit-test it — the package's serialization will not be testable enough. Include a event_id (UUID) and occurred_at in every payload; receivers will need them for dedupe and ordering.

Version and sign what you send. Compute an HMAC of the raw JSON body with a per-subscriber secret and send it as a header (X-CaptainHook-Signature plus a timestamp). Do this in your own job wrapper or a small middleware around the package's HTTP call. Unsigned payloads mean any party who learns a customer's endpoint URL can forge events. Document the verification snippet for subscribers before you onboard the first one.

Use the Eloquent model as the admin surface. The package's webhook model (active toggle, URL, event) is exactly what an internal admin page or Nova resource needs. Build the toggle and the "test send" button against that model rather than the config file, so Support can disable a broken subscriber without a deploy. This is the single highest-leverage UI you can add.

Read the delivery log as your support tool. "We never got the event" is the ticket you will get. Make sure every attempt — success and failure — writes URL, event, payload, response code, response body (truncated), and duration. Without this the package is undebuggable in production. A webhook:replay {delivery_id} Artisan command that re-dispatches a stored job pays for itself the first week.

Wrap non-trivial logic in Artisan commands and jobs. Anything beyond "POST this array" belongs in your app, not inline in the config or in a subclass of the package job. You want it testable, you want it reusable from a replay command, and you want it in your own namespace when you eventually fork the package.

Testing pattern: dispatch the underlying model event in a feature test, Bus::fake() / Queue::fake() and assert the expected webhook job was dispatched with the right payload; then a separate test that runs the job with Http::fake() (or a Guzzle mock) and asserts the request body, headers, and signature, plus that the delivery record is updated on both 2xx and 5xx. Do a manual end-to-end against a request bin only for the initial wiring.

Gotchas and Tips

Abandonment is the headline risk. Last release December 2016, zero Packagist dependents. On Laravel 8+/PHP 8 expect Composer resolution conflicts and drift in the HTTP client and event dispatcher. Don't burn a day on --ignore-platform-reqs — fork the handful of classes you need into app/ or a private package, pin them, and own the code. Budget for that from day one rather than treating it as a surprise.

Verify the doc against the source. The supplied material contains a contradictory assessment describing this as a Git hooks installer. It isn't. Read src/ for the actual config keys, job class, and event names; the README from 2016 will be partially wrong.

No delivery guarantees out of the box. Confirm in the source whether the job retries, backs off, or dead-letters. Likely not. You'll need $tries, $backoff, and failed() handling on the job, plus auto-disable of a subscriber after N consecutive failures (you already have the delivery log — query it in a scheduled command). Without this, outages are silent.

SSRF if URLs are user-supplied. Resolve the host before POSTing and reject private ranges, loopback, and 169.254.169.254. Require HTTPS. If only first-party endpoints exist this is low risk; the moment a customer enters a URL it's a critical one.

Idempotency and ordering. Queued jobs don't guarantee order and retries duplicate deliveries. Always send an event ID and document that receivers must dedupe. Don't promise ordered delivery — you can't provide it cheaply.

sync queue driver. The most common misconfiguration. It makes outbound HTTP latency part of your user's request and swallows failures into the request cycle. Assert in config and in CI that webhooks run on a real queue connection.

Serialization leakage. The default payload may include the whole model, timestamps, internal IDs, and sometimes relationships. Whitelist fields explicitly; grep the payload in a test for columns you'd never want a customer to see.

Log hygiene. Response bodies can contain tokens or PII from the receiver. Truncate to a few KB, redact headers, and set a retention window on the delivery table — it will grow unbounded otherwise.

Timeouts and HTTP client drift. Set an explicit connect/read timeout on the request (3–5s) and treat timeouts as failures worth logging. On PHP 8 the underlying Guzzle version may behave differently than in 2016; test the failure path explicitly.

Outbound only. This package does nothing for receiving third-party webhooks, which is a different problem (routing, signature verification, replay protection on your side). Don't let the shared vocabulary confuse scope.

Observability. Emit a metric or log line per delivery attempt (subscriber, event, status, duration) and alert on failure-rate. Without it, the delivery table is a graveyard nobody reads until a customer complains.

Debugging recipe: reproduce the dispatch in php artisan tinker, run the queued job with --once and -v, and inspect both the delivery row and the receiver's logs. If the row shows a 2xx and the customer says nothing arrived, the bug is on their side or in your URL — the stored response body will tell you which.

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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment