1. Go to this page and download the library: Download artisanpack-ui/bookings library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
use ArtisanPackUI\Bookings\Enums\SeriesEditScope;
$recurring = app( SeriesService::class );
// One week moves; the rule is untouched and that occurrence stops following it.
$recurring->edit( $series, SeriesEditScope::This, [ 'start_time' => $newStart ], $occurrence );
// The rule is bounded here, and the new series it returns carries the change forward.
$tail = $recurring->edit( $series, SeriesEditScope::ThisAndFollowing, [ 'rrule' => '…' ], $occurrence );
// The rule is rewritten and everything still to come is re-derived from it.
$recurring->edit( $series, SeriesEditScope::All, [ 'rrule' => '…' ] );
$recurring->cancel( $series, BookingActor::Customer, 'Moving away.' );
use ArtisanPackUI\Bookings\Services\ManageTokenService;
$tokens = app( ManageTokenService::class );
// Minted automatically when a booking is created — take it once, for the email.
$token = $booking->pullPlainManageToken();
$booking = $tokens->findBooking( $requestToken ); // null when the token is unknown
$tokens->verifyFor( $booking, $requestToken ); // hash_equals, never ==
$fresh = $tokens->issueFor( $booking ); // the old link stops working here
use ArtisanPackUI\Bookings\Contracts\SmsDriver;
class TwilioSmsDriver implements SmsDriver
{
public function send( string $phone, string $message ): void
{
// Throw if the gateway refuses it. The send is recorded against
// booking_notification_log as failed, the other channels carry on,
// and an operator has something to read.
}
}
'retention' => [
'prune_after_days' => 365 * 3, // read by bookings:prune
'notification_log_days' => 90,
'webhook_delivery_days' => null, // falls back to webhooks.delivery_retention_days
'calendar_events_ttl_days' => 30,
],
route( 'artisanpack.bookings.admin.bookings' ); // the list
route( 'artisanpack.bookings.admin.settings' ); // general config
Gate::define( 'bookings.manage', fn ( User $user ) => $user->isStaff() );
use ArtisanPackUI\Bookings\MeetingTypes\RegisteredMeetingType;
addFilter( 'ap.bookings.registeredMeetingTypes', function ( array $types ): array {
$types[] = new RegisteredMeetingType(
'webinar',
'Webinar',
'Broadcast to many attendees at once.',
allowsMultipleAttendees: true,
);
return $types;
} );
use ArtisanPackUI\Bookings\Support\HookRegistry;
HookRegistry::all(); // every hook, with its type and the issue that fires it
HookRegistry::shipped(); // the ones firing today — the table above
HookRegistry::pending(); // declared, not yet fired
use ArtisanPackUI\Bookings\Support\HookSubscriptions;
HookSubscriptions::whenInstalled( 'forms', function (): void {
addFilter( 'ap.forms.fieldTypes', /* ... */ );
} );
text
GET api/bookings/manage/{token}
POST api/bookings/manage/{token}/cancel { "reason": "optional" }
POST api/bookings/manage/{token}/reschedule { "start_time": "2026-06-01T19:00:00+00:00" }