Docs
WordPress plugin

Webhooks and hooks

How the plugin receives and verifies Agoo webhooks, and the WordPress actions and filters you can use, with PHP examples.

Planned· P9For WordPress developers

This is designed and scheduled but not built yet. We document it now so you can plan your integration.

Agoo for WordPress includes a webhook receiver. When something happens in Agoo, such as a visitor checking in, Agoo sends a signed event to your site, the plugin verifies it, and then it fires WordPress actions that your theme or plugin can hook into. Filters let you change what the blocks and shortcodes print.

Set up the receiver first: see receive Agoo events. It needs the Growth plan or above.

The receiver

The plugin registers one REST route:

POST /wp-json/agoo/v1/webhooks

For each request, it:

  1. Reads the raw body and the webhook-id, webhook-timestamp and webhook-signature headers.
  2. Verifies the Standard Webhooks signature with your webhook signing secret, and rejects timestamps more than 5 minutes from your server's clock.
  3. Skips events it has already processed, by webhook-id. Processed IDs are remembered for 3 days, longer than Agoo's retry schedule.
  4. Fires agoo_webhook_received, then the action for that event type if there is one, such as agoo_visit_checked_in.
  5. Records the ID as processed and responds.
ResponseWhen
200The event was processed, or was a duplicate.
400The signature, timestamp or body is invalid. Nothing runs.
500One of your hook callbacks threw an exception. The event isn't marked as processed, so Agoo retries it later.
503No webhook signing secret is configured.

Agoo waits up to 15 seconds for a response and retries failures for about 27 hours. See webhooks.

The route has no WordPress login: Agoo can't sign in to your site. Instead, nothing runs until the signature has been verified, so only Agoo, holding your endpoint's secret, can trigger your hooks.

Keep hooks fast

Your callbacks run while Agoo waits for the response. Do quick work directly, and hand anything slow (calls to other services, emails, imports) to a background job, as in the CRM example. If your callbacks take longer than 15 seconds, Agoo counts the delivery as failed and sends it again.

Hooks run without a logged-in user

During a webhook request, there is no current WordPress user (get_current_user_id() returns 0), so current_user_can() is always false. Don't gate your callback on capabilities: the signature check is the authorisation. If your callback creates posts or other content, set the author explicitly.

Actions

agoo_webhook_received

Fires for every verified event, of every type.

do_action( 'agoo_webhook_received', array $event );

$event is the decoded event as an associative array:

$event
[
    'id'              => 'evt_01jpb0nr5yhcf05g59s2s1kke5',
    'type'            => 'visit.checked_in',
    'created_at'      => '2026-10-14T09:31:02Z',
    'organisation_id' => 'org_01kjpsrza0etzb0a4vf72x8ang',
    'livemode'        => true,
    'data'            => [ 'object' => [ /* the visit, booking, delivery… */ ] ],
]

The objects inside data.object are the same as the API's. See event types.

Log every event type (for debugging)
add_action( 'agoo_webhook_received', function ( array $event ): void {
	if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
		error_log( sprintf( 'Agoo event %s (%s)', $event['type'], $event['id'] ) );
	}
} );

agoo_visit_checked_in

Fires for visit.checked_in events, after agoo_webhook_received, with the visit.

do_action( 'agoo_visit_checked_in', array $visit );

$visit is $event['data']['object']: the visit as the API returns it, including id, status, site_id, checked_in_at and the visitor.

For any other event type, use agoo_webhook_received and check $event['type'].

Filters

agoo_embed_attributes

Changes the data-agoo-* attributes of a widget before it's printed.

apply_filters( 'agoo_embed_attributes', array $attributes, string $widget, array $settings );
ParameterContains
$attributesAttribute name ⇒ value, such as 'data-agoo-booking' => 'btype_…'. See the embed's data attributes.
$widget'booking' or 'preregister'.
$settingsThe block attributes or shortcode attributes as given.

Return the attributes array. Values are escaped when printed. Attributes the embed doesn't recognise are ignored.

Prefill the phone number from a WooCommerce customer
add_filter( 'agoo_embed_attributes', function ( array $attributes, string $widget, array $settings ): array {
	if ( 'booking' !== $widget || ! is_user_logged_in() ) {
		return $attributes;
	}

	$phone = get_user_meta( get_current_user_id(), 'billing_phone', true );
	if ( is_string( $phone ) && '' !== $phone ) {
		$attributes['data-agoo-phone'] = $phone;
	}

	return $attributes;
}, 10, 3 );

The same caching caution applies as for Prefill user: only add personal details for logged-in users, on pages your cache doesn't store for them.

agoo_embed_script_url

Changes the URL the embed script is loaded from.

apply_filters( 'agoo_embed_script_url', string $url );

The default is https://js.agoo.ardent.africa/v1/embed.js. Use the filter only if your security policy requires serving third-party scripts through your own reverse proxy, and serve the script unmodified:

Serve the script through your own proxy
add_filter( 'agoo_embed_script_url', function ( string $url ): string {
	return 'https://static.example.org/vendor/agoo/v1/embed.js';
} );

The widget's frame is still loaded from https://app.agoo.ardent.africa.

Examples

Post to Slack when a visitor checks in

Coastline Consult wants a message in its #reception Slack channel when a guest arrives. Create a Slack incoming webhook for the channel, put its URL in wp-config.php, and add this to a small plugin or your theme's functions.php:

wp-config.php
define( 'MYSITE_SLACK_WEBHOOK_URL', 'https://hooks.slack.com/services/…' );
wp-content/mu-plugins/agoo-slack.php
<?php
/**
 * Plugin Name: Agoo arrivals to Slack
 */

add_action( 'agoo_visit_checked_in', function ( array $visit ): void {
	if ( ! defined( 'MYSITE_SLACK_WEBHOOK_URL' ) ) {
		return;
	}

	$name    = $visit['visitor']['name'] ?? 'A visitor';
	$company = $visit['visitor']['company'] ?? '';
	$time    = wp_date( 'H:i', strtotime( $visit['checked_in_at'] ), new DateTimeZone( 'Africa/Accra' ) );

	$text = $company
		? sprintf( '%s (%s) checked in at %s.', $name, $company, $time )
		: sprintf( '%s checked in at %s.', $name, $time );

	$response = wp_remote_post( MYSITE_SLACK_WEBHOOK_URL, [
		'headers' => [ 'Content-Type' => 'application/json' ],
		'body'    => wp_json_encode( [ 'text' => $text ] ),
		'timeout' => 5,
	] );

	if ( is_wp_error( $response ) ) {
		error_log( 'Agoo to Slack failed: ' . $response->get_error_message() );
	}
} );

A Slack post is quick, so it runs directly. A failure is logged rather than thrown, so a Slack outage doesn't make Agoo retry the event. Only post what the channel's members are allowed to see: visitor names are personal data.

Add a CRM contact when a visitor checks in

To add each visitor to your CRM, schedule the CRM call as a background job so the webhook response stays fast:

wp-content/mu-plugins/agoo-crm.php
<?php
/**
 * Plugin Name: Agoo visitors to CRM
 */

// 1. On check-in, queue a job with only the fields the CRM needs.
add_action( 'agoo_visit_checked_in', function ( array $visit ): void {
	$visitor = $visit['visitor'] ?? [];
	if ( empty( $visitor['email'] ) ) {
		return; // nothing to match the contact on
	}

	wp_schedule_single_event( time(), 'mysite_agoo_add_crm_contact', [
		[
			'agoo_visit_id' => $visit['id'],
			'name'          => $visitor['name'] ?? '',
			'email'         => $visitor['email'],
			'phone'         => $visitor['phone'] ?? '',
			'company'       => $visitor['company'] ?? '',
		],
	] );
} );

// 2. The job calls the CRM's API. Define MYSITE_CRM_TOKEN in wp-config.php.
add_action( 'mysite_agoo_add_crm_contact', function ( array $contact ): void {
	$response = wp_remote_post( 'https://crm.example/api/v2/contacts', [
		'headers' => [
			'Authorization' => 'Bearer ' . MYSITE_CRM_TOKEN,
			'Content-Type'  => 'application/json',
		],
		'body'    => wp_json_encode( [
			'name'    => $contact['name'],
			'email'   => $contact['email'],
			'phone'   => $contact['phone'],
			'company' => $contact['company'],
			'source'  => 'Agoo visit ' . $contact['agoo_visit_id'],
		] ),
		'timeout' => 15,
	] );

	if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) >= 300 ) {
		error_log( 'CRM contact failed for ' . $contact['agoo_visit_id'] );
	}
} );

WP-Cron runs scheduled events on the next page load. On a low-traffic site, set up a real cron job for wp-cron.php, or use a job library such as Action Scheduler. Before copying visitor data into another system, make sure your privacy notice covers it and your CRM keeps it no longer than you need: under Act 843, you're responsible for where visitor data goes.

Verifying signatures yourself

The plugin verifies every event for you. If you write your own receiver instead, for example in a theme or on a site without the plugin, this function verifies an Agoo webhook in plain PHP:

Verify an Agoo webhook
/**
 * Verifies an Agoo webhook (Standard Webhooks). Returns true only for a genuine, fresh event.
 */
function mysite_agoo_verify( string $body, string $id, string $timestamp, string $signatures, string $secret, int $tolerance = 300 ): bool {
	if ( '' === $id || ! ctype_digit( $timestamp ) || '' === $signatures ) {
		return false;
	}
	if ( abs( time() - (int) $timestamp ) > $tolerance ) {
		return false;
	}

	// The key is the base64 part of the secret, after "whsec_".
	$key = base64_decode( preg_replace( '/^whsec_/', '', $secret ), true );
	if ( false === $key ) {
		return false;
	}
	$expected = base64_encode( hash_hmac( 'sha256', "{$id}.{$timestamp}.{$body}", $key, true ) );

	// The header can hold several space-separated "v1,<signature>" values.
	foreach ( explode( ' ', $signatures ) as $entry ) {
		[ $version, $signature ] = array_pad( explode( ',', $entry, 2 ), 2, '' );
		if ( 'v1' === $version && hash_equals( $expected, $signature ) ) {
			return true;
		}
	}
	return false;
}
A custom REST route that uses it
add_action( 'rest_api_init', function (): void {
	register_rest_route( 'mysite/v1', '/agoo-events', [
		'methods'             => 'POST',
		'callback'            => 'mysite_handle_agoo_event',
		'permission_callback' => '__return_true', // Agoo can't log in; the signature is checked below
	] );
} );

function mysite_handle_agoo_event( WP_REST_Request $request ) {
	$body = $request->get_body(); // the raw body: verify before decoding

	$valid = mysite_agoo_verify(
		$body,
		(string) $request->get_header( 'webhook-id' ),
		(string) $request->get_header( 'webhook-timestamp' ),
		(string) $request->get_header( 'webhook-signature' ),
		MYSITE_AGOO_WEBHOOK_SECRET
	);
	if ( ! $valid ) {
		return new WP_Error( 'agoo_invalid_signature', 'Invalid signature.', [ 'status' => 400 ] );
	}

	$event = json_decode( $body, true );
	// De-duplicate on webhook-id, then handle $event['type'] …

	return new WP_REST_Response( null, 204 );
}

hash_equals compares in constant time, so the check doesn't leak how much of a signature matched. The scheme is the same in every language; see verify signatures.

Capabilities

WhoCanCapability
AdministratorsSee and change Settings → Agoo, including keys and the webhook secretmanage_options
Editors, authors and contributorsAdd and configure the blocks and shortcodes in content they can editedit_posts
AnyoneSee and use the widgets on published pagesnone
Agoo (signed requests only)Trigger the webhook actionsnone: verified by signature

The block editor's lists of booking types and sites are loaded with the publishable key, so editors never see the secret key.

Multisite

  • Install the plugin once for the network. You can network-activate it or activate it on individual sites.
  • Settings are per site. Each site has its own Settings → Agoo, keys and webhook secret. Sites can connect to different Agoo organisations, such as a school group's campuses with separate accounts, or to the same one.
  • Each site has its own receiver URL, such as https://example.org/tema/wp-json/agoo/v1/webhooks. Create a webhook endpoint in Agoo for each site that should receive events.
  • Constants in wp-config.php apply to every site in the network. Use them only if all sites connect to the same organisation; otherwise use the per-site settings.
  • Deleting the plugin removes its settings from every site.

On this page