PHPackages                             goldnead/statamic-booking - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. goldnead/statamic-booking

ActiveStatamic-addon

goldnead/statamic-booking
=========================

Records Cal.com bookings in Statamic: signed webhooks, idempotent, with a seam for what happens next.

v1.0.0(today)01↑2900%proprietaryPHPPHP ^8.2CI passing

Since Aug 24Pushed todayCompare

[ Source](https://github.com/goldnead/statamic-booking)[ Packagist](https://packagist.org/packages/goldnead/statamic-booking)[ RSS](/packages/goldnead-statamic-booking/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (5)Versions (2)Used By (0)

Statamic Booking
================

[](#statamic-booking)

> Records Cal.com bookings in Statamic — signed, idempotent, and out of your way.

This addon does **not** build a calendar. Availability, time zones, reschedules and reminders are a solved problem, and solving them again badly is the usual way a booking feature goes wrong. Cal.com does that part; this records what it decided, and gives your site a place to react.

Requirements
------------

[](#requirements)

Statamic 6 · PHP 8.2+ · a database · a Cal.com account (the free plan is enough).

Installation
------------

[](#installation)

```
composer require goldnead/statamic-booking
php artisan migrate
php please vendor:publish --tag=statamic-booking-config
```

Then define at least one endpoint and point Cal.com at it.

Usage
-----

[](#usage)

Define an endpoint, point Cal.com at it, and listen for what arrives.

### Endpoints

[](#endpoints)

One entry per funnel. A free consultation and a paid lesson are different things — different event types, different secrets, usually different consequences.

```
// config/statamic-booking.php
'endpoints' => [
    'beratung' => [
        'secret' => env('BOOKING_SECRET_BERATUNG'),
        'label' => 'Kostenloses Erstgespräch',
    ],
],
```

In Cal.com: **Settings → Webhooks → New**, subscriber URL `https://your-site.test/!/statamic-booking/beratung`, secret the same value, triggers `BOOKING_CREATED`, `BOOKING_RESCHEDULED`, `BOOKING_CANCELLED` — and, if your event type needs confirming, `BOOKING_REQUESTED` and `BOOKING_REJECTED` as well.

A requested booking is recorded but is **not** upcoming: it has been asked for, not agreed, and showing it as an appointment is how a calendar tells its owner a lie. A rejected one is closed the same way a cancellation is.

**An endpoint without a secret refuses every request.** That is deliberate: an unverified booking webhook is an open write endpoint, and "the site has not been configured yet" must not mean "anyone may post here".

### Reacting

[](#reacting)

Three events, each dispatched **once per real change** — never on a redelivery, so a listener may assume it is being told something new:

```
use Goldnead\StatamicBooking\Events\BookingMade;

Event::listen(BookingMade::class, function (BookingMade $event) {
    $event->booking->email;        // who booked
    $event->booking->scheduled_at; // when
    $event->booking->endpoint;     // which funnel
});
```

`BookingRescheduled` and `BookingCancelled` work the same way. A cancellation **keeps** the row and stamps `cancelled_at`: "there was an appointment and it was cancelled" is a different fact from "there never was one", and only one of them can be reconstructed later.

### Tags

[](#tags)

TagParametersWhat it does`{{ bookings }}``endpoint`, `limit`Upcoming bookings, soonest first; cancelled, rejected and merely requested ones left out`{{ bookings:count }}``endpoint`How many there are```
{{ bookings endpoint="beratung" limit="3" }}
    {{ if no_results }}
        Zurzeit sind keine Termine eingetragen.
    {{ else }}
        {{ scheduled_at format="d.m.Y H:i" }} — {{ duration_minutes }} min
    {{ /if }}
{{ /bookings }}
```

**The tags carry no names, addresses — or titles.** Cal.com's default booking title is "30 Min Meeting between {organiser} and {attendee}", which is a field that looks harmless and carries the booker. One careless template is all it takes to publish the people who booked, so the tag simply has nothing to publish. Whoever needs the rest has the model.

Configuration
-------------

[](#configuration)

Every key lives in `config/statamic-booking.php`.

KeyDefaultWhat happens when it is wrong`endpoints`noneAn endpoint without a `secret` refuses every request. Renaming a handle after the first booking orphans every row that carries it.`signature.header``X-Cal-Signature-256`A wrong header name means every delivery is refused as unsigned.`signature.timestamp_header``null`Without one, a captured delivery can be replayed forever. Naming a header your provider does not send refuses every delivery.`signature.tolerance_seconds``300`Too tight and clock drift refuses real deliveries; too loose and a replay window opens.`rate_limit``60`Per minute, per IP.`keep_days``730`Null keeps every name and address forever, which is the opposite of data minimisation.What it stores
--------------

[](#what-it-stores)

Endpoint, the provider's id, status, appointment time and time zone, duration, name, address, meeting URL, and the event title. `php please booking:prune` deletes bookings whose appointment is older than `keep_days` (default 730). Null keeps everything, which is a decision, not a default.

Security
--------

[](#security)

- **Every delivery is verified** with HMAC-SHA256 over the raw body, compared in constant time. A valid signature over a *different* body is refused — that is the forgery a naive check lets through.
- **Each endpoint has its own secret.** One leaked secret does not open the others.
- **Replay:** if your provider sends a timestamp, name the header in `signature.timestamp_header`. It is then **signed together with the body** (`timestamp.body`, Stripe's scheme) and deliveries outside `tolerance_seconds` are refused. Checking an *unsigned*timestamp header would be theatre: whoever replays a captured delivery simply writes the current time into it. **Cal.com sends no timestamp today.** Without one this endpoint is signature-authentic but not replay-proof: someone who captured a valid delivery can send it again. Said plainly rather than implied.
- The route drops CSRF on purpose. The caller is a server and authenticates with a signature, which is a stronger proof than a session token and the only one a provider can give.

Multi-site
----------

[](#multi-site)

Bookings are not site-scoped. A booking is an appointment with a person, not a piece of content, and it does not become a different appointment when read from another site.

Support
-------

[](#support)

Only the latest version is supported.

Changelog · License
-------------------

[](#changelog--license)

[CHANGELOG.md](CHANGELOG.md) · [LICENSE.md](LICENSE.md)

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/85572d690277234a86834808cab169c4900922b4855fe9028426f8350dd74e97?d=identicon)[goldnead](/maintainers/goldnead)

---

Top Contributors

[![goldnead](https://avatars.githubusercontent.com/u/1313348?v=4)](https://github.com/goldnead "goldnead (1 commits)")

---

Tags

statamicappointmentsbookingStatamic addoncalcom

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/goldnead-statamic-booking/health.svg)

```
[![Health](https://phpackages.com/badges/goldnead-statamic-booking/health.svg)](https://phpackages.com/packages/goldnead-statamic-booking)
```

###  Alternatives

[statamic/statamic

Statamic

832182.1k](/packages/statamic-statamic)[statamic-rad-pack/runway

Eloquently manage your database models in Statamic.

137236.2k8](/packages/statamic-rad-pack-runway)[statamic/seo-pro

71548.3k](/packages/statamic-seo-pro)[duncanmcclean/statamic-cargo

Comprehensive e-commerce addon for Statamic. Build bespoke e-commerce sites without the complexity.

3622.8k](/packages/duncanmcclean-statamic-cargo)[cboxdk/statamic-mcp

MCP (Model Context Protocol) server for Statamic CMS v6 — gives AI assistants structured access to content, blueprints, assets, and more.

3219.8k](/packages/cboxdk-statamic-mcp)[aerni/advanced-seo

Comprehensive SEO addon for Statamic with flexibility in mind

1818.9k](/packages/aerni-advanced-seo)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
