---
title: "Webhooks"
source: https://dev.pageseeder.com/api/usage/webhooks.html
description: "Webhooks enable PageSeeder to notify client applications via HTTP POST requests when data changes. Configurable by administrators, they support various event types across projects, groups, and servers in XML or JSON format."
last_updated: 2026-09-08T14:08:15+10:00
tokens: ~2984
---

# Webhooks

Webhooks allow client apps to be notified by HTTP post requests when data changes in PageSeeder. A webhook can be configured to only send requests for certain event types, [projects](/reference/glossary/project.md), [groups](/reference/glossary/group.md) or for server events, so it can be tailored to specific client apps.

## Events

The following event types are supported by webhooks:

### Admin

- webhook.ping

### Group or project events

- comment.created
- comment.modified
- comment.archived
- comment.unarchived
- comment.deleted
- group.deleted, group.archived, group.modified
- groupproperties.modified
- groupfolder.created, groupfolder.deleted groupfolder.modified
- membership.created
- membership.modified
- membership.deleted
- publication.created
- publication.modified
- publication.deleted
- task.updated (changes to any `<task>` attributes or content).
- workflow.updated (changes to any `<workflow>` attributes or content).
- uri.created (includes upload).
- uri.modified (includes move, edit, etc. – any time `uri/@lastmodified` is changed).
- uri.archived
- uri.deleted

### Project events

- group.created
- project.deleted, project.archived, project.modified

### Server events

- member.created
- member.modified
- member.deleted
- project.created
- server.started *(requires PageSeeder v5.99 or higher)*

*A uri/comment/task move between groups and a group/project rename produces a `deleted` and possible `created` (and `modified` for uri) event with the relevant groups for each.*

*A task/workflow/URL change produces a separate event for each group, as the content for each group can be different.*

## Requests

- Webhook POST requests can be in XML or JSON format (shown following).
- Each request contains one or more webevents.
- Each webevent contains zero or more event groups (these are the PageSeeder groups the event affects).
- Each webevent also contains an object in either **minimal** (private and public IDs only) or **basic** format depending on the webhook configuration.
- For delete events, the object always has **minimal** format.

The top level `<webhook>` element only has an `@id` attribute and optional `@name` attribute. See [\<webevent\>](/api/elements/element_webevent.md) element for more details on the request content.

### XML format

```xml
<webevents>
  <webhook id="123" [name="xyz"] />
  <webevent id="[object id].[nano time]"
            datetime="2012-11-23T22:35:12+10:00"
            type="[object].[action]">
    <event>
      <group id="" name="">
      [<group .../> ...]
    </event>
    <webhook|uri|comment|task|workflow|membership|member|...
             id=""
             [name="[group name]"]
             [docid=""] [path=""]
             [username=""]
             [email=""]
             [...]>
        [...]
    </...>
  </webevent>
  [<webevent ...> ... </webevent> ...]
</webevents>
```

### JSON format

```json

{
  "webhook": {
    "id" : "123", 
    "name" : "xyz"
  },
  "webevents": [
    { 
      "id": "[object id].[nano time]", 
      "datetime": "2012-11-23T22:35:12+10:00", 
      "type": "[object].[action]",
      "event": [
        {"id":"", "name":""}
      ], 
      "webhook|uri|comment|task|...": {
        ...
      }
    },
    ...
  ]
}
```

## Configuration

Webhooks can only be configured by PageSeeder administrators using the [API](/api.md) or the user interface by selecting the *system administration \>* **OAuth \> Webhooks** page and following the steps below.

### Step 1: Register a client

On the **OAuth** \> **Clients** page click, the **Register new client** button. Enter the details required and click **Register**.

Most of the details are related to [OAuth](https://oauth.net/2) configuration so that the client app can also connect to PageSeeder and access data.

If a **Webhook secret** is entered, it is used to sign all POST requests to this client with an HMAC of the request body and sends it in an  `X-PS-Signature` HTTP header. This is so the app can verify the POST is coming from PageSeeder. Following is an example Java code snippet that could be used to verify the signature:

```java
SecretKeySpec key = new SecretKeySpec(
  webhook_secret.getBytes(StandardCharsets.UTF_8), "AES");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
String signature = Base64.getEncoder().encodeToString(mac.doFinal(body));
```

### Step 2: Start the client app

When a webhook is registered, it immediately sends a `webhook.ping` event POST to the URL for the webhook. If the client app replies with HTTP 200 and HTTP header `X-PS-Secret` set to the same value as was sent in that header on the POST, then the webhook status changes from “pending” to “active”.

This confirms that the client wishes to receive webhook POST requests from PageSeeder and helps guard against PageSeeder sending unwanted or SPAM requests. It is similar to the immediate confirmation mechanism described here [http://resthooks.org/docs/security/](http://resthooks.org/docs/security).

> **Tip:** To avoid unnecessary warnings, ensure the client app is running before registering a webhook. If you cannot, don’t forget to initiate a `webhook.ping` event from the **Webhooks** page, once the app is accessible, to update the webhook status to “active”.

### Step 3: Register a webhook

On the **OAuth** \> **Webhooks** page, click the **Register new webhook** button. Choose the client, enter a URL plus other details (see following) if required and click **Register**.

- **Support insecure SSL **– means that the SSL certificate is not checked when sending requests. This can be useful for development servers but must not be used for production.
- **Object type: minimal** – means that only private and public IDs are returned for the object (no other content), see [\<webevent\>](/api/elements/element_webevent.md) for more detail on the minimal type.
- **Event filters** – means the events that are sent to this webhook. Multiple event types can be specified by selecting different event types repeatedly.
- **Include server events** – means include the following events which are not specific to a project or group: `member.created, member.modified, member.deleted, project.created, server.started`.

Check the **Webhooks** page to ensure the status is “active”. Event POST requests are only sent to “active” webhooks.

## Troubleshooting

Following are the possible status values for a webhook and the remedies required:

| Status | Description | Remedy |
| --- | --- | --- |
| **pending** | Awaiting confirmation of ping request | Start client app (as above) and click Ping on Webhooks page |
| **active** | Ready to receive event POST requests | n/a |
| **disabled** | Manually stopped from receiving events | Change status to “active” by clicking Ping on Webhooks page |
| **warning** | Has failed POST requests that are still being retried | Fix problem with webhook URL and click Ping on Webhooks page to test |
| **unreachable** | Has reached the maximum number of retries for failed POST requests (some events might be lost) | Fix problem with webhook URL and click Ping on Webhooks page to test. Optionally clear or update the client app cache for the missing events. |
| **error** | Other fatal error (some events might be lost) | Click Ping on Webhooks page to test. Optionally clear or update the client app cache for the missing events. |

An error is displayed on the **Administration dashboard** if there are any webhooks with a status of **warning, unreachable** or **error**. The status of each webhook is displayed on the **Webhooks** page. For further detail, view the **Recent logs** page.

> **Note:** “Client app cache” is PageSeeder data cached by an external app. This is different from the following Webhook caches, which store jobs and events to continue processing if PageSeeder restarts.

### Webhook caches

The following message might appear on the **Administration dashboard** page:

*The webhooks caches were corrupted probably due to an unclean shutdown so unprocessed events before the shutdown will not have been sent (see general logs).*

In this case, view the **General logs** for the day the server was shutdown and look for the following lines in the general log to see if there were any unprocessed events. If any numbers are non-zero or these lines are missing, the client app caches might need to be cleared or updated for the missing events.

```text
There was 0 unprocessed webhook events at shutdown.
There was 0 unprocessed webhook jobs at shutdown.
There was 0 unprocessed failed webhook jobs at shutdown.
```

The contents of the webhook caches can be viewed on the **Server status \> Caching** page. Click **Info** next to the `webhook-events`, `webhook-failed-jobs` or `webhook-jobs` cache, then click each entry to see detailed information.

### Failed POST requests

Following are the rules used to process failed POST requests to a webhook URL:

- If POST fails with `3xx` or `410` error, set webhook status to `unreachable` and stop sending to that webhook.
- If POST fails with `4xx`, `5xx` or other error increment, `RetryNumber` and retry again using exponential backoff after `2retries x 10` seconds (e.g. 10s, 20s, 40s, 80s, ... ~12 hrs for `webhookRetries=12`). Also, set webhook status to `warning`.
- If `RetryNumber > webhookRetries` (see following), set webhook status to `unreachable` and stop sending to that webhook

> **Note:** PageSeeder tries to send failed requests in the order they were created, but the client app must always check the event datetime stamp because events might be received out of order.

### Global properties

If there are issues with memory, performance or failure processing, the following [global properties](/guide/configuration/properties/global_properties.md) can be adjusted to modify the system.

| **Property** | **Description** | **Default value** |
| --- | --- | --- |
| `webhookRequestInterval` | The number of seconds to collect webhook events into a batch before sending as a single request (`0` means `0.5` seconds). | `5` |
| `webhookRequestSize` | The maximum number of bytes in a single webhook request (requests are split if they exceed this limit). | `1400` |
| `webhookRetries` | The maximum number of times a failed webhook request is resent before the webhook status is changed to “unreachable”. | `12` |

### Sample Webhook handler

The following example in Java provides the scaffolding for handling webhook payloads on your client app.

```java
@WebServlet(urlPatterns = "/webhook")
public class WebhookHandler extends HttpServlet {

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse res) 
      throws IOException {
    // We're not returning any content
    res.setStatus(HttpServletResponse.SC_NO_CONTENT);

    // If the secret is specified, send it back for pings
    String secret = req.getHeader("X-PS-Secret");
    if (secret != null) {
      res.setHeader("X-PS-Secret", secret);
    }

    // Get the payload
    byte[] payload = req.getInputStream().readAllBytes();

    // If the payload is signed
    String signature = req.getHeader("X-PS-Signature");
    if (signature != null) {
      boolean signatureFailed = false;
      try {
        SecretKeySpec key = new SecretKeySpec("my_secret".getBytes(StandardCharsets.UTF_8), "AES");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(key);
        String signatureCheck = Base64.getEncoder().encodeToString(mac.doFinal(payload));
        if (!signature.equals(signatureCheck)) {
          res.sendError(HttpServletResponse.SC_BAD_REQUEST);
          signatureFailed = true;
        }
      } catch (GeneralSecurityException ex) {
        res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        signatureFailed = true;
      }
      if (signatureFailed) return;
    }

    // Handle the data here...
    String data = new String(payload, StandardCharsets.UTF_8);

  }

}
```

