Gareth Wilson Gareth Wilson

Guide to Nextcloud Webhooks: Features and Best Practices

Published


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

FeatureDetails
Appwebhook_listeners, bundled with Nextcloud server
RegistrationPOST /ocs/v2.php/apps/webhook_listeners/api/v1/webhooks, or the Orchestration Gateway app
PermissionsAdministrator, or delegated "Webhooks" rights
SubscriptionOne event class per listener, with optional eventFilter and userIdFilter
MethodhttpMethod, chosen per listener
BodyJSON: event, user, time, plus authentication
Event fieldevent.class, the fully-qualified PHP class name
AuthenticationauthMethod: "header" with authData headers. No signature or timestamp
DispatchBackground jobs: every 5 minutes with the default cron, sooner with dedicated workers
RetriesNone documented. A non-2xx response is logged
Source IPsYour own server

Common events

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

AreaEvent classes
Files and foldersOCP\Files\Events\Node\NodeCreatedEvent, NodeWrittenEvent, NodeTouchedEvent, NodeDeletedEvent, and BeforeNodeCreatedEvent, BeforeNodeWrittenEvent, BeforeNodeTouchedEvent, BeforeNodeReadEvent, BeforeNodeDeletedEvent
Copy, rename, restoreNodeCopiedEvent, NodeRenamedEvent, NodeRestoredEvent and their Before* versions
System tagsOCP\SystemTag\TagAssignedEvent, TagUnassignedEvent
CalendarOCP\Calendar\Events\CalendarObjectCreatedEvent, CalendarObjectUpdatedEvent, CalendarObjectDeletedEvent, CalendarObjectMovedToTrashEvent, CalendarObjectRestoredEvent, CalendarObjectMovedEvent
Forms appOCA\Forms\Events\FormSubmittedEvent
Tables appOCA\Tables\Event\RowAddedEvent, RowUpdatedEvent, RowDeletedEvent
Mail appOCA\Mail\Event\NewMessageReceivedEvent, MessageSentEvent, MessageFlaggedEvent, MessageDeletedEvent

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

{
  "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 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):

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.

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:

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 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 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 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 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.

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.

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 sits in front of your receiver, checks the header, and keeps every event until your service has processed it.

Get started with Hookdeck for free and handle Nextcloud webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.