Skip to content

Webhooks ​

Outgoing webhooks to Zapier, Make, n8n or any URL: published posts, comments, orders, bookings.

kitloom/webhooks · admins: kitloom/webhooks-filament, kitloom/webhooks-nova

Outgoing webhooks: when a post is published, a comment posted, an order placed or a booking made, the site POSTs a signed JSON body to the addresses you set up — Zapier, Make, n8n, a Slack workflow, a receiver of your own. Calls go out from the queue, are retried, and every one is in a log. The admin is kitloom/webhooks-filament or kitloom/webhooks-nova.

Setting up a webhook ​

  1. In the receiving service, create a webhook trigger ("Catch Hook" in Zapier, "Custom webhook" in Make, a Webhook node in n8n) and copy the URL it gives you.
  2. Webhooks → Add: a name, that URL, the topics you want, and keep the secret the form generated (or paste the receiver's, when it gives one).
  3. Save, then "Send test": the receiver gets a ping and the admin says what it answered. Deliveries show every call since, with the body and the answer.

The URL must be a public address on the internet. The site refuses a webhook to its own server or network — localhost, 127.0.0.1, the private ranges (10.…, 192.168.…), the cloud's metadata address 169.254.169.254 — and a host that does not resolve: the receiver's answer is shown in the log, so such a webhook would let whoever sets it up read what the server sees inside its network. The address is checked when the webhook is saved and again before every call, since the name may point elsewhere by then; a refused call fails the delivery for good. A redirect is not followed: a receiver that answers 3xx has not taken the delivery — use the address it redirects to. A receiver on the same server or network (a self-hosted n8n) needs WEBHOOKS_ALLOW_PRIVATE_HOSTS=true in the server's environment; it cannot be turned on from the admin.

A webhook sends while it is active. Disabled is yours: nothing is sent, the webhook stays. Paused is the module's: the deliveries failed too many times in a row (Settings → Pause after failures, 5 unless set) — the receiver is down or the URL is wrong — and nothing more is sent until you resume it ("Resume" in the list, or save the webhook again). A test delivery goes out whatever the status.

Topics ​

TopicWhenThe body's data
post.publishedA post, page or record of another type went live — created published, or published from a draft, a pending or a scheduled onethe post
post.updatedA published post was saved again (not the save that published it, not a draft)the post as it is now
post.trashedA post went to the trashthe post as it was
post.deletedA post is gone for goodid, type, slug, title
comment.createdA comment or product review was posted, whatever status it gotthe comment, with status
comment.approvedA waiting, spam or trashed comment was approvedthe comment
order.createdAn order was placed at the checkout (it may still wait for the gateway) — with kitloom/shopthe order
order.updatedAn order moved to another status — paid, completed, cancelled, refunded, on holdthe order, with previous_status
order.refundedMoney went back on an order, all of it or a partthe order, with refund
customer.createdA customer made an account in the shopid, email, name
booking.createdA booking was made — on the site, in the admin, by an agent — with kitloom/bookingsthe booking
booking.updatedA booking was confirmed, declined, cancelled, completed or marked a no-showthe booking, with previous_status
pingA test sent by hand; every webhook gets it, it is not subscribed tomessage, webhook

The post topics send only posts of the types the site shows to visitors — public types: posts, pages, products, the types of the active theme and of Post Types. What a module keeps in the posts table for itself is never sent: a contact form's messages (with the sender's name, address and words), orders, coupons, menu items, revisions, and any type the site does not declare public. ignore_types leaves out public types too; a shop's own rows in the comments table (ignore_comment_types) are no comments and never sent, and neither is a comment on a record of a type that is not public. A post is post.published once, even when the save that published it is followed by another in the same request.

The post's body: id, type, status, slug, title, excerpt and content as written (the receiver renders or strips the HTML as it likes), link, author (id, name), parent_id, date and modified (UTC), terms under each taxonomy (id, name, slug), featured_image (id, and url when kitloom/media is installed). The comment's: id, post_id, post_title, type, status (approved, pending, spam, trash), author_name, author_email, author_url, user_id, content, parent_id, date. An order is what the shop shows the customer on the "order received" page (number, status, email, customer, items, totals, billing, shipping, …) with its id; a booking is what the booking module tells everyone (code, status, service, staff, start, end, name, email, …) with its id and note.

What the receiver gets ​

A POST with Content-Type: application/json and these headers:

HeaderValue
X-Kitloom-Topicthe topic, post.published
X-Kitloom-Deliverythe delivery's id, a UUID — the same as id in the body; a retry carries the same one
X-Kitloom-Webhookthe webhook's id
X-Kitloom-SignatureHMAC-SHA256 of the raw body with the webhook's secret, hex

The body:

json
{
  "id": "6f1c0c6e-2a3b-4a0d-9d2e-5b1f3b7a0c11",
  "topic": "post.published",
  "created_at": "2026-10-18T12:00:00Z",
  "site": "https://example.com",
  "actor": {"id": 1, "name": "Ada"},
  "data": {"id": 42, "type": "post", "status": "publish", "slug": "hello", "title": "Hello", "…": "…"}
}

actor is who did it — the event's user, or whoever was signed in — and null for a visitor or the system. A receiver that must trust the call checks the signature over the body exactly as received, before parsing it:

php
$expected = hash_hmac('sha256', $request->getContent(), $secret);
abort_unless(hash_equals($expected, $request->header('X-Kitloom-Signature', '')), 403);

Zapier, Make and n8n accept the call as it is; the headers are there for a receiver of your own. Any 2xx answer is a success. Anything else, a timeout or no connection is a failed attempt: the call is tried again after a growing wait (3 attempts unless configured) and the delivery is then failed for good. The receiver's answer — the code and the first 2000 characters of the body — is kept with the delivery, and "Send again" makes a new delivery of the same body.

Where things are kept ​

WhatWhere
Webhookstable webhooks (the site's table prefix in front): name, URL, secret, topics (JSON), status, failures in a row, last answer — model Kitloom\Webhooks\Models\Webhook, key webhook in the ModelRegistry
Deliveriestable webhook_deliveries: the body as sent, attempts, the answer, the error, how long it took — model Kitloom\Webhooks\Models\WebhookDelivery, key webhook_delivery
Settingsoptions webhooks_retention_days (30 unless set; 0 keeps every delivery) and webhooks_failure_limit (5; 0 never pauses)
The callsspatie/laravel-webhook-server's job on the queue (kitloom-webhooks.queue, connection)

The secret is kept as it is, as WooCommerce keeps its own: the owner reads it again to paste it into the receiver. Whoever may manage webhooks (manage_webhooks, administrators) reads it.

webhooks:prune deletes deliveries older than the retention (--days for one run); the module schedules it daily — the application's schedule:run has to run. It runs on one server of several, by a lock in the cache, so a site on several servers needs a shared cache store.

Without a queue worker (QUEUE_CONNECTION=sync) a call is made while the page that caused it waits — up to the timeout, 5 seconds unless configured — and gets one attempt: there is nobody to retry it. A site with webhooks wants a worker.

From code ​

php
use Kitloom\Webhooks\Webhooks;

$webhooks = app(Webhooks::class);
$webhook = $webhooks->create(['name' => 'Zapier', 'url' => 'https://hooks.zapier.com/…', 'topics' => ['post.published']], $actor);
$webhooks->ping($webhook, $actor);          // a test delivery
$webhooks->resume($webhook, $actor);        // after a pause
$webhooks->resend($delivery, $actor);       // the same body again

With an actor the service refuses what they may not do; without one it is the system.

A topic of your own — a module's or the application's: a Kitloom\Webhooks\Contracts\WebhookTopic (its name, label, group, the events it listens to, whether an event counts, and the data of the body) tagged WebhookTopic::TAG in a provider's register(). It is then on the form, in the agents' description of the site, and its events are listened to; one named like a built-in topic replaces it.

php
$this->app->tag([ReviewPublished::class], WebhookTopic::TAG);

AI agents ​

With kitloom/ai installed, an agent whose user has manage_webhooks lists, creates and changes webhooks (webhooks_list, webhooks_create — which answers the secret once — webhooks_update, also with ping: true for a test delivery) and reads the log (webhooks_deliveries); describe_site tells it the topics and how a delivery is signed. Nothing is deleted: a webhook is disabled instead. kitloom-webhooks.ai.register false — no webhook tools.

Personal data ​

A delivery keeps a copy of what went out, and an event about a comment, an order or a customer carries a person's email address — for the retention days (30 unless set). With kitloom/privacy installed, Tools → Export Personal Data lists the deliveries with the address in the body sent or in the receiver's answer: the event, the receiving service, when, how it went. Erase Personal Data deletes them from the log. A delivery still in the queue stays until it ends — the call goes out from the queue's copy anyway — and the report counts it as kept; erase again once it has. The services the webhooks sent to keep their own copies, and the report says to ask them. kitloom-webhooks.privacy.register false leaves the log out of both.

Configuration ​

config/kitloom-webhooks.php (php artisan vendor:publish --tag=kitloom-webhooks-config):

  • queue, connection — where the calls are queued; the application's defaults unless set.
  • tries (3), timeout (5 seconds), verify_ssl (true) — how a call is made. On the sync driver a call gets one attempt whatever tries says.
  • allow_private_hosts (false, WEBHOOKS_ALLOW_PRIVATE_HOSTS) — receivers on the server's own network are allowed too.
  • ignore_types, ignore_comment_types — post and comment types never sent; for posts on top of the rule that only public types are.
  • schedule — the daily pruning.
  • ai.register, privacy.register, filament.register, nova.register.

Tests ​

From the application that installs the package: php vendor/bin/phpunit --testsuite Webhooks.

kitloom — modules and themes for Laravel.
Pricing · Services · License · Refunds · Privacy · Community · [email protected]