A booking widget on your site
Add online booking to your own website with the embed script, send people to a thank-you page, and have your server told about every booking.
This is designed and scheduled but not built yet. We document it now so you can plan your integration.
Goal
Akwaaba Clinic wants patients to book a consultation from the clinic's own website, not a separate page. When someone books, the front desk should get an email straight away with the details, so they can prepare the file before the patient arrives.
You'll:
- Add the booking widget to a page with the embed script.
- Send the patient to your own thank-you page after booking.
- Receive the
booking.createdwebhook on your server and email the front desk, with PHP or TypeScript.
Agoo already confirms the booking to the patient by SMS or email and reminds them before the appointment. In-person bookings also become expected visits, so the patient checks in at the kiosk on arrival.
If the clinic turns on Requires confirmation for the booking type, booking.created arrives with status: "pending" until a doctor confirms the request, then booking.confirmed follows. The email below includes the status so the front desk can tell the two apart; subscribe to booking.confirmed as well if you want an email when a request is accepted.
What you need
| Need | Details |
|---|---|
| Plan | The widget works on any plan with booking pages. The webhook needs Growth or above in live mode. Test mode works on every plan. |
| Keys | A publishable key (agoo_pk_…) for the page |
| Webhook endpoint | Subscribed to booking.created (and booking.cancelled if you want cancellations too, booking.confirmed if the booking type requires confirmation) |
| A booking type | Published with public visibility, such as "General consultation" |
| Server | PHP 8.1 or later with the PDO SQLite extension, or Node.js 20 or later |
Steps
Add the widget to your page
Copy the booking type's ID (btype_…) from its page in the console, and use your test publishable key while you build.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Book a consultation · Akwaaba Clinic</title>
<script src="https://js.agoo.ardent.africa/v1/embed.js" defer></script>
</head>
<body>
<main>
<h1>Book a consultation</h1>
<p>Choose a time that suits you. We'll confirm by SMS and remind you the day before.</p>
<div
data-agoo-booking="btype_01jpm5q1nxw1rnj47e65gh2bh8"
data-agoo-key="agoo_pk_test_…"
data-agoo-theme="auto"
data-agoo-color-primary="#0b5d4b"
></div>
</main>
<script>
// After a booking, go to your own thank-you page. Only the booking ID is passed, never personal details.
window.addEventListener("agoo:booked", (event) => {
window.location.href = "/thank-you.html?ref=" + encodeURIComponent(event.detail.booking_id)
})
</script>
</body>
</html>Create a simple public/thank-you.html page for people to land on. If your site sends a Content Security Policy, allow the script and frame as described in embed: Content Security Policy.
Create the webhook endpoint
In the test-mode console, open Console → Developers → Webhooks, add an endpoint with your server's URL (for example https://www.akwaabaclinic.example/agoo-webhook.php) subscribed to booking.created, and copy its signing secret (whsec_…). While you build on your laptop, use agoo listen instead (see Try it in test mode).
Receive the webhook and email the front desk
<?php
declare(strict_types=1);
require __DIR__ . '/../src/agoo_verify.php';
$secret = getenv('AGOO_WEBHOOK_SECRET') ?: '';
$body = file_get_contents('php://input') ?: '';
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
// 1. Verify the signature before trusting anything in the request.
$valid = agoo_verify_webhook(
$body,
$id,
$_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '',
$_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '',
$secret
);
if ( ! $valid ) {
http_response_code( 400 );
exit( 'Invalid signature' );
}
// 2. Skip events we've already handled. Agoo can deliver the same event more than once.
$db = new PDO( 'sqlite:' . __DIR__ . '/../data/agoo.sqlite' );
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$db->exec( 'CREATE TABLE IF NOT EXISTS processed_webhooks (id TEXT PRIMARY KEY, processed_at TEXT NOT NULL)' );
$seen = $db->prepare( 'SELECT 1 FROM processed_webhooks WHERE id = ?' );
$seen->execute( [ $id ] );
if ( $seen->fetchColumn() ) {
http_response_code( 204 );
exit;
}
// 3. Handle the event.
$event = json_decode( $body, true, 512, JSON_THROW_ON_ERROR );
if ( 'booking.created' === $event['type'] ) {
$booking = $event['data']['object'];
$start = ( new DateTimeImmutable( $booking['start_at'] ) )
->setTimezone( new DateTimeZone( $booking['time_zone'] ) )
->format( 'l j F Y, H:i' );
$lines = [
'New booking: ' . $start,
'Status: ' . $booking['status'], // confirmed, or pending until the host confirms
'Patient: ' . $booking['attendee']['name'],
'Phone: ' . ( $booking['attendee']['phone'] ?? 'not given' ),
'Email: ' . ( $booking['attendee']['email'] ?? 'not given' ),
'Booking: ' . $booking['id'],
];
$message = implode( "\n", $lines );
$to = getenv( 'FRONT_DESK_EMAIL' ) ?: '';
if ( '' === $to ) {
error_log( $message ); // local testing: write to the log instead of sending
} elseif ( ! mail( $to, 'New booking: ' . $start, $message ) ) {
http_response_code( 500 ); // not recorded below, so Agoo retries later
exit( 'Email failed' );
}
}
// 4. Record the event as handled, then acknowledge it.
$db->prepare( 'INSERT INTO processed_webhooks (id, processed_at) VALUES (?, ?)' )
->execute( [ $id, gmdate( DATE_ATOM ) ] );
http_response_code( 204 );<?php
declare(strict_types=1);
/**
* Verifies an Agoo webhook (Standard Webhooks). Returns true only for a genuine, fresh event.
*/
function agoo_verify_webhook( string $body, string $id, string $timestamp, string $signatures, string $secret, int $tolerance = 300 ): bool {
if ( '' === $id || ! ctype_digit( $timestamp ) || '' === $signatures || '' === $secret ) {
return false;
}
if ( abs( time() - (int) $timestamp ) > $tolerance ) {
return false;
}
$key = base64_decode( preg_replace( '/^whsec_/', '', $secret ), true );
if ( false === $key ) {
return false;
}
$expected = base64_encode( hash_hmac( 'sha256', "{$id}.{$timestamp}.{$body}", $key, true ) );
foreach ( explode( ' ', $signatures ) as $entry ) {
[ $version, $signature ] = array_pad( explode( ',', $entry, 2 ), 2, '' );
if ( 'v1' === $version && hash_equals( $expected, $signature ) ) {
return true;
}
}
return false;
}Create a data/ folder next to public/ that the web server can write to. mail() uses your server's mail setup. Many hosts require an SMTP library such as PHPMailer or Symfony Mailer instead; swap it in at the same place. Keep the data/ folder outside your web root, as here, so the database can't be downloaded.
Try it in test mode
Serve the page and the receiver locally
For the PHP version:
AGOO_WEBHOOK_SECRET=whsec_… php -S localhost:8000 -t publicForward test webhooks
agoo listen --forward-to http://localhost:8000/agoo-webhook.php --events booking.createdagoo listen prints a signing secret for the session. Restart the PHP server with that value as AGOO_WEBHOOK_SECRET.
Book a slot
Open http://localhost:8000/book.html, choose a time and book with your own details. The widget shows "Test mode"; the confirmation SMS appears in the console's test outbox instead of being sent. Your browser goes to the thank-you page, agoo listen shows the event forwarded with 204, and with FRONT_DESK_EMAIL unset, the email text appears in the PHP server's log.
You can also fire the event without the page: agoo trigger booking.created.
Production checklist
- Replace
agoo_pk_test_…with your live publishable key, and check the booking type is public in live mode. - Create a live webhook endpoint over HTTPS, subscribed to
booking.created, and set its secret on the server. - Set
FRONT_DESK_EMAIL, and confirm your server can send email. - Add the embed's sources to your Content Security Policy, if you have one.
- Exclude
embed.jsfrom any script optimisation (combining, minifying or delaying). - Keep the booking type's intake questions to what reception needs. Don't ask for clinical details in a booking form. A public booking type's questions are public, so mark any field for your staff as staff only.
- Make sure the front-desk mailbox is only read by staff who should see patient bookings.