Webhooks and hooks
How the plugin receives and verifies Agoo webhooks, and the WordPress actions and filters you can use, with PHP examples.
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/webhooksFor each request, it:
- Reads the raw body and the
webhook-id,webhook-timestampandwebhook-signatureheaders. - Verifies the Standard Webhooks signature with your webhook signing secret, and rejects timestamps more than 5 minutes from your server's clock.
- Skips events it has already processed, by
webhook-id. Processed IDs are remembered for 3 days, longer than Agoo's retry schedule. - Fires
agoo_webhook_received, then the action for that event type if there is one, such asagoo_visit_checked_in. - Records the ID as processed and responds.
| Response | When |
|---|---|
200 | The event was processed, or was a duplicate. |
400 | The signature, timestamp or body is invalid. Nothing runs. |
500 | One of your hook callbacks threw an exception. The event isn't marked as processed, so Agoo retries it later. |
503 | No 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:
[
'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.
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 );| Parameter | Contains |
|---|---|
$attributes | Attribute name ⇒ value, such as 'data-agoo-booking' => 'btype_…'. See the embed's data attributes. |
$widget | 'booking' or 'preregister'. |
$settings | The block attributes or shortcode attributes as given. |
Return the attributes array. Values are escaped when printed. Attributes the embed doesn't recognise are ignored.
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:
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:
define( 'MYSITE_SLACK_WEBHOOK_URL', 'https://hooks.slack.com/services/…' );<?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:
<?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:
/**
* 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;
}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
| Who | Can | Capability |
|---|---|---|
| Administrators | See and change Settings → Agoo, including keys and the webhook secret | manage_options |
| Editors, authors and contributors | Add and configure the blocks and shortcodes in content they can edit | edit_posts |
| Anyone | See and use the widgets on published pages | none |
| Agoo (signed requests only) | Trigger the webhook actions | none: 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.phpapply 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.
Related
Blocks and shortcodes
Every setting of the Agoo booking and pre-registration blocks and shortcodes, with examples for a church and a clinic.
Google and Microsoft calendars
Connect Google Workspace or Microsoft 365 calendars so booking pages respect hosts' real availability, bookings land in their calendars, and meeting invites carry Meet or Teams links.