# Guide to Nextcloud Webhooks: Features and Best Practices

Nextcloud webhooks let your self-hosted Nextcloud server tell other systems what is happening inside it: a file is uploaded, changed or deleted, a tag is assigned, a calendar event changes, a form is submitted or a row is added to a table. Teams use them to start document workflows, feed automation platforms, index content and keep external systems in step with their Nextcloud data.

This guide covers how the webhook_listeners app works, how to register a listener with a secret header, the events and the envelope they share, how background jobs shape delivery, and the practices that make a Nextcloud receiver dependable.

## What are Nextcloud webhooks?

Webhooks come from the **webhook_listeners** app, which is bundled with Nextcloud server. Enable it with `occ app:enable webhook_listeners`. A **listener** ties one event class to one URI: when that event fires on your server, Nextcloud sends a request with the event as a JSON body.

You register listeners through the OCS API, or through the optional **Orchestration Gateway** app, which adds a settings page for managing them. Registering needs administrator rights, or membership of a group that has been given delegated administration rights for "Webhooks".

Each listener sets its own HTTP method, so it isn't always `POST`. It can also filter events, send fixed headers, and request short-lived app tokens that let your receiver call back into Nextcloud as a user.

Nextcloud doesn't sign webhook requests. A listener's `authMethod` is either `none` or `header`. With `header`, the key-value pairs in `authData` are added to the request headers, and that secret header is what your receiver checks.

## Nextcloud webhook features

| Feature | Details |
| --- | --- |
| App | `webhook_listeners`, bundled with Nextcloud server |
| Registration | `POST /ocs/v2.php/apps/webhook_listeners/api/v1/webhooks`, or the Orchestration Gateway app |
| Permissions | Administrator, or delegated "Webhooks" rights |
| Subscription | One event class per listener, with optional `eventFilter` and `userIdFilter` |
| Method | `httpMethod`, chosen per listener |
| Body | JSON: `event`, `user`, `time`, plus `authentication` |
| Event field | `event.class`, the fully-qualified PHP class name |
| Authentication | `authMethod: "header"` with `authData` headers. No signature or timestamp |
| Dispatch | Background jobs: every 5 minutes with the default cron, sooner with dedicated workers |
| Retries | None documented. A non-`2xx` response is logged |
| Source IPs | Your own server |

## Common events

Nextcloud lists the events that core and its bundled apps provide, and installed apps can register more.

| Area | Event classes |
| --- | --- |
| Files and folders | `OCP\Files\Events\Node\NodeCreatedEvent`, `NodeWrittenEvent`, `NodeTouchedEvent`, `NodeDeletedEvent`, and `BeforeNodeCreatedEvent`, `BeforeNodeWrittenEvent`, `BeforeNodeTouchedEvent`, `BeforeNodeReadEvent`, `BeforeNodeDeletedEvent` |
| Copy, rename, restore | `NodeCopiedEvent`, `NodeRenamedEvent`, `NodeRestoredEvent` and their `Before*` versions |
| System tags | `OCP\SystemTag\TagAssignedEvent`, `TagUnassignedEvent` |
| Calendar | `OCP\Calendar\Events\CalendarObjectCreatedEvent`, `CalendarObjectUpdatedEvent`, `CalendarObjectDeletedEvent`, `CalendarObjectMovedToTrashEvent`, `CalendarObjectRestoredEvent`, `CalendarObjectMovedEvent` |
| Forms app | `OCA\Forms\Events\FormSubmittedEvent` |
| Tables app | `OCA\Tables\Event\RowAddedEvent`, `RowUpdatedEvent`, `RowDeletedEvent` |
| Mail app | `OCA\Mail\Event\NewMessageReceivedEvent`, `MessageSentEvent`, `MessageFlaggedEvent`, `MessageDeletedEvent` |

Every body shares one envelope. Here is Nextcloud's example for a new file:

```json
{
  "event": {
    "class": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
    "node": {
      "id": 437,
      "path": "/admin/files/test-webhook.txt"
    }
  },
  "user": {
    "uid": "admin",
    "displayName": "Admin"
  },
  "time": 1700100000
}

```

* `event.class` names the event. The backslashes are escaped in JSON, so the parsed value is `OCP\Files\Events\Node\NodeCreatedEvent`.
* `user` is the user who triggered the event, or `null` when there was no user session.
* `time` is a Unix timestamp in seconds.

Copy, rename and restore events carry `source` and `target` objects instead of `node`. A node that no longer exists, as in `NodeDeletedEvent`, may have a `path` but no `id`. Use the matching `Before*` event if you need the ID before deletion. Tag events carry `objectType`, `objectIds` and `tagIds`, and calendar events carry `calendarId`, `calendarData`, `shares` and `objectData`.

The sender also adds an `authentication` key before sending, holding any app tokens the listener asked for with `tokenNeeded`. Those tokens let you call back into Nextcloud and are valid for an hour, so treat the body as a credential and keep it out of logs.

> **Inspect Nextcloud webhooks as they arrive.** Register a listener with a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to see exactly what your server sends for each event class, with no account or setup required.

## Setting up Nextcloud webhooks

Choose a header name and a long random value first, for example `Authorization` and `Bearer ` followed by the output of `openssl rand -hex 32`.

Register a listener with an admin account (an app password is fine):

```bash
curl -u "admin:$NEXTCLOUD_APP_PASSWORD" \
  -H "OCS-APIRequest: true" \
  -H "Content-Type: application/json" \
  -X POST "https://cloud.example.com/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks" \
  -d '{
    "httpMethod": "POST",
    "uri": "https://example.com/webhooks/nextcloud",
    "event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
    "headers": { "Content-Type": "application/json" },
    "authMethod": "header",
    "authData": { "Authorization": "Bearer YOUR_LONG_RANDOM_VALUE" }
  }'

```

* `event` is the class name, with each backslash escaped once for JSON.
* `headers` is sent as given. The sender adds no `Content-Type` of its own, so set it here if your receiver needs one.
* `eventFilter` narrows what fires. For example, `{ "event.node.path": "/^\\/.*\\/files\\/Invoices\\//" }` matches only files under an `Invoices` folder, and `{ "user.uid": "bob" }` only Bob's events. It supports operators such as `$in`, `$exists` and `$and`.
* `occ webhook_listeners:list` shows every registered listener.

To route through Hookdeck, create a source with the **Nextcloud** type and use its URL as the `uri`. Enter the `authData` header name in **API Key Header** and its exact value, including any `Bearer ` prefix, in **API Key**. The source pre-selects `POST` but doesn't manage the method, so if a listener uses another `httpMethod`, such as `PUT`, allow that method on the source too.

## Securing Nextcloud webhooks

Read your header case-insensitively and compare its whole value with the stored one, in constant time. Register a route for each method your listeners use.

```javascript
const crypto = require('crypto');
const express = require('express');

const HEADER = (process.env.NEXTCLOUD_WEBHOOK_HEADER || 'Authorization').toLowerCase();
const SECRET = process.env.NEXTCLOUD_WEBHOOK_SECRET; // the exact, whole header value

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function handleNextcloud(req, res) {
  const received = req.get(HEADER);
  if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
    return res.sendStatus(401);
  }

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Route on payload.event.class. Don't log payload.authentication.
  res.sendStatus(200);
}

const app = express();
// Register the httpMethod(s) your listeners use; add app.get, app.patch or app.delete the same way for those.
app.post('/webhooks/nextcloud', express.raw({ type: '*/*' }), handleNextcloud);
app.put('/webhooks/nextcloud', express.raw({ type: '*/*' }), handleNextcloud);

```

The same check in Python:

```python
import hmac
import os

HEADER = os.environ.get("NEXTCLOUD_WEBHOOK_HEADER", "Authorization")
SECRET = os.environ.get("NEXTCLOUD_WEBHOOK_SECRET", "")

def verify_nextcloud_request(headers) -> bool:
    if not SECRET:
        return False  # fail closed when unconfigured
    received = headers.get(HEADER)  # Starlette and Flask headers are case-insensitive
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))

```

A static header authenticates the sender, not the body. It can't detect a modified payload, and with no signed timestamp a captured request can be replayed and still pass. Use HTTPS between your server and the receiver, a long random value, and update the listener's `authData` if it might have leaked.

> **Make Nextcloud webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks your secret header on every Nextcloud event, stores each one durably, and retries delivery to your service, which the webhook_listeners app doesn't do.

## Nextcloud webhook limitations and pain points

### One attempt per event

**The Problem:** Events sent while your receiver is down never arrive.

**Why It Happens:** The webhook_listeners app sends each event as a single background job. Its sender makes one request and logs a non-`2xx` response or an error as a warning, and Nextcloud documents no retry.

**Workarounds:**

* Keep the receiver highly available, and respond as soon as the header checks out.
* Watch the Nextcloud log for webhook warnings, and reconcile from Nextcloud's APIs after an outage.

**How Hookdeck Can Help:** Hookdeck accepts each request as soon as your server sends it and stores it, then delivers to your service with [retries](/docs/retries) you configure, so an outage on your side no longer loses events.

### Events arrive minutes late

**The Problem:** A file appears in Nextcloud and your workflow starts several minutes later.

**Why It Happens:** Webhooks are dispatched by background jobs. With the default 5-minute cron interval, events wait for the next run.

**Workarounds:**

* Run dedicated workers for the `OCA\WebhookListeners\BackgroundJobs\WebhookCall` job, as Nextcloud's admin manual describes, with four or more in parallel.
* Don't depend on arrival order. Use `time` from the envelope.

**How Hookdeck Can Help:** Hookdeck records when each event arrived and lets you replay events in order after a fix.

### Deleted nodes lose their IDs

**The Problem:** A delete handler can't find the record to remove because the event has no `id`.

**Why It Happens:** When a node no longer exists, Nextcloud represents it with an internal placeholder that has a path but no ID.

**Workarounds:**

* Key file records on path as well as ID, or listen for `BeforeNodeDeletedEvent`, which still has the ID.
* Use optional access for `node.id`, `source.id` and `target.id`.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/filters) can route `Before*` and post events to different destinations, so each handler only sees the events it needs.

### The body can carry tokens

**The Problem:** Logs and debugging tools end up holding credentials for your Nextcloud server.

**Why It Happens:** Listeners that request `tokenNeeded` get app tokens in the body's `authentication` key, valid for an hour.

**Workarounds:**

* Only request tokens when the receiver needs to call back into Nextcloud.
* Strip `authentication` before you log or store the event.

**How Hookdeck Can Help:** A Hookdeck [transformation](/docs/transformations) can remove the `authentication` key before the event reaches your destination. Hookdeck stores the original request, so factor that into your retention settings if you request tokens.

## Best practices

### Route on `event.class`

Compare the class name exactly, backslashes included, and acknowledge classes you don't handle. Installed apps can add events at any time.

### Filter at the source

Use `eventFilter` and `userIdFilter` so Nextcloud only sends the events you need. Filtering on `event.node.path` with a regular expression is the usual way to watch one folder.

### Deduplicate deliveries

The envelope has no event ID. Build a key from `event.class`, the node ID or path, the user and `time`. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Background workers wait for each request, so a slow receiver slows dispatch for everyone. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

The Nextcloud webhook_listeners app sends one request per event class you register, with a JSON envelope of `event`, `user` and `time`, a method you choose and no signature. Register listeners with `authMethod: "header"` and compare the whole header value, route on `event.class`, run dedicated workers if you need events quickly, and plan for the fact that a failed request isn't retried.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the header, and keeps every event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Nextcloud webhooks reliably in minutes.