Docs
Recipes

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.

Planned· P9For developers and web teams

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:

  1. Add the booking widget to a page with the embed script.
  2. Send the patient to your own thank-you page after booking.
  3. Receive the booking.created webhook 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

NeedDetails
PlanThe widget works on any plan with booking pages. The webhook needs Growth or above in live mode. Test mode works on every plan.
KeysA publishable key (agoo_pk_…) for the page
Webhook endpointSubscribed to booking.created (and booking.cancelled if you want cancellations too, booking.confirmed if the booking type requires confirmation)
A booking typePublished with public visibility, such as "General consultation"
ServerPHP 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.

public/book.html
<!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

public/agoo-webhook.php
<?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 );
src/agoo_verify.php
<?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:

Terminal 1
AGOO_WEBHOOK_SECRET=whsec_… php -S localhost:8000 -t public

Forward test webhooks

Terminal 2
agoo listen --forward-to http://localhost:8000/agoo-webhook.php --events booking.created

agoo 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.js from 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.

On this page