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_. 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_, 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:
{
"event": {
"class": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
"node": {
"id": 437,
"path": "/admin/files/test-webhook.txt"
}
},
"user": {
"uid": "admin",
"displayName": "Admin"
},
"time": 1700100000
}
event.classnames the event. The backslashes are escaped in JSON, so the parsed value isOCP\Files\Events\Node\NodeCreatedEvent.useris the user who triggered the event, ornullwhen there was no user session.timeis 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" }
}'
eventis the class name, with each backslash escaped once for JSON.headersis sent as given. The sender adds noContent-Typeof its own, so set it here if your receiver needs one.eventFilternarrows what fires. For example,{ "event.node.path": "/^\\/.*\\/files\\/Invoices\\//" }matches only files under anInvoicesfolder, and{ "user.uid": "bob" }only Bob's events. It supports operators such as$in,$existsand$and.occ webhook_shows every registered listener.listeners:list
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\WebhookCalljob, as Nextcloud's admin manual describes, with four or more in parallel. - Don't depend on arrival order. Use
timefrom 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.idandtarget.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
authenticationbefore 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.