coinvestor/larahook
LaraHook brings WordPress-style actions and filters to Laravel. Register and run hooks anywhere in your app or packages, prioritize callbacks, and extend behavior without editing core code. A lightweight, familiar API for clean, decoupled customization.
Larahook is CoInvestor's webhook layer for Laravel: it hangs off Eloquent model events so you can notify external systems (or internal services) when a record is created, updated, or deleted without writing dispatch code at every call site.
Minimal path to a first webhook:
composer require co-investor/larahook (1.4.3 is the current release; it is the first line to declare Laravel 13 support).php artisan vendor:publish with the Larahook provider/tag from the README — so you get the config file and any webhook delivery table/migrations. Run php artisan migrate.HasWebhooks trait to the model you want to emit for.Where to look first, in order:
config/larahook.php — the single source of truth for the queue connection/queue name, retry and backoff policy, HTTP timeout, and signing secret. Read this before writing any webhook definitions; most "why isn't it firing / why is it slow" questions are answered here.vendor/co-investor/larahook/src/...) — it registers the model event listeners, so it tells you exactly which events are hooked and what gets serialized.config when you need different behaviour.First use case worth wiring: an outbound "order.created" to a partner or an internal event bus. Keep it on a dedicated queue (see gotchas) rather than default, because webhook traffic is bursty and you don't want it competing with mail and notifications.
Model-event driven (the default). Declare endpoints alongside the model so ownership sits with the domain object. This is the pattern to prefer for CRUD-shaped resources: create/update/delete map cleanly to order.created, order.updated, order.deleted, and the package handles the dispatch plumbing.
Explicit dispatch for domain events. Model events are a poor fit for things that aren't 1:1 with a row change — a payment captured, a refund settled, a state machine transition. Call the package's dispatch entry point from an event listener or a domain action class instead, so the webhook fires once per business event rather than once per save().
Payload shaping via API Resources. Do not let the model serialize itself. Wrap it in a JsonResource (or an explicit array) before it goes on the queue. Two reasons: you control the wire contract independently of schema changes, and you avoid leaking columns you forgot about. Keep a version field (v, or a dated event name) in the payload so consumers can evolve with you.
Queue everything, always. A synchronous webhook delivery inside a request is a latency bug and a timeout bug. Put deliveries on the queue, configure retries with exponential backoff in config/larahook.php, and set a hard HTTP timeout (5–10s is usually right). Treat "delivered" as best-effort with retries, not as a transaction.
Signing and verification. Set a per-environment secret and sign the payload (HMAC over the raw body, header-based, timestamped). Document the verification snippet for consumers — this is the part integrators get wrong most often. Rotate secrets by accepting two keys during a window rather than cutting over instantly.
Idempotency for consumers. Because retries and at-least-once delivery are inherent, include a stable delivery/event ID in the payload and headers so consumers can dedupe. Cheap to add now, painful to retrofit.
Testing. Queue::fake() (or Bus::fake()) plus an assertion that the job was pushed for the right model and event; Http::fake() plus Queue::fake() released to run the job for payload/signature assertions. Keep one contract test per webhook type asserting the exact JSON shape — that's your regression net when a Resource changes.
Local development. Point endpoints at a tunnel (ngrok/Expose) or at a local sink route that logs the request. If the package verifies TLS on outbound calls, be aware self-signed local endpoints will fail — use a real tunnel host rather than disabling verification.
Order::query()->update([...]), upsert, and raw DB writes fire no model events, so no webhooks. If a bulk operation must notify, dispatch explicitly after it commits.saveQuietly(), withoutEvents(), and deleteQuietly() are deliberate event suppressors. Useful for seeding, dangerous if it creeps into production code paths.sync and you use after_commit = false, a worker can pick the job up before the transaction commits and read stale/missing data. Either set after_commit on the queue connection or dispatch from an DB::afterCommit callback. Related: on deleted, capture relations before the delete, since cascades may already have removed them by the time the job runs.config/larahook.php, run php artisan config:cache on deploy (and config:clear locally). A surprising number of "my config change did nothing" reports are stale cached config.illuminate/* constraint may have bumped the minimum PHP version, and if you're on an older Laravel you should confirm 1.4.3 still satisfies your pin (it should — the constraint is usually widened, not swapped). Run your webhook contract tests after the bump; a framework major can change queued-job serialization and Http client defaults even when the package itself is unchanged.failed() handler (or the package's failure hook) to your logging/alerting. Silent webhook death is the classic failure mode: retries exhaust, nobody notices, downstream data drifts for a week. The delivery log table is only useful if something is watching it.How can I help you explore Laravel packages today?