# Guide to Egnyte Webhooks: Features and Best Practices

Egnyte webhooks push activity from an Egnyte domain to your own service in near real time. When someone uploads a file, moves a folder, shares a link, changes a permission or approves a workflow task, Egnyte can tell your application straight away, which is what you need to keep a search index or local cache in sync, log access changes for compliance, or start a business process when a document lands in a folder.

This guide covers how Egnyte webhooks are registered and filtered, the batched payload and event types, how to authenticate requests with the `authHeader` you set, Egnyte's strict response and retry rules, and the practices that make an Egnyte receiver reliable.

## What are Egnyte webhooks?

Egnyte webhooks are managed through the Webhooks API, at `https://{domain}.egnyte.com/pubapi/v1/webhooks`. Your application registers a webhook with an OAuth 2.0 token that includes the `Egnyte.webhooks` scope, naming the HTTPS URL to call, the event types it wants and, optionally, the folder paths to watch. Egnyte then sends an HTTP `POST` to that URL whenever a matching event happens in the domain. Webhooks are the push counterpart of the Events API, which you would otherwise have to poll.

Each request body is an object with a `data` array, so one delivery can carry more than one event. Every item names its own `eventType`.

Egnyte does not sign webhook requests. When you register a webhook you can set an `authHeader`, and Egnyte sends that value as the `authorization` header on every delivery. Egnyte asks you to validate it on every incoming request.

## Egnyte webhook features

| Feature | Details |
| --- | --- |
| Management | Webhooks API: register (`POST`), list, details, status, update (`PUT`), delete |
| API authentication | OAuth 2.0 Bearer token with the `Egnyte.webhooks` scope |
| Event filtering | Exact names or category wildcards (`fs:*`, `link:*`, `comment:*`, `permission:*`, `meta:*`). Omitting `eventType` subscribes to all events |
| Path filtering | Up to 100 comma-separated folder paths. Omitting `path` watches every path |
| Limits | The number of webhooks per client ID and domain is limited. Each registration has an `expires` timestamp |
| Delivery | `POST`, `Content-Type: application/json`, body `{"data": [...]}` |
| Authentication | `authorization` header set to your registered `authHeader`, verbatim |
| Signature and timestamp header | None |
| TLS | HTTPS with a valid CA-signed certificate. Self-signed certificates are rejected |
| Success | Exactly HTTP `200` |
| Retries | Immediately, then after 1 minute, 10 minutes and 1 hour (4 attempts). Then the webhook is disabled |
| Source IPs | Not published |

## Common events

Egnyte documents 36 event types in seven categories. Wildcards exist for File System, Links, Comments, Permissions and Metadata. Workflow and group events must be named individually.

| Category | Events |
| --- | --- |
| File System (`fs:*`) | `fs:add_file`, `fs:add_folder`, `fs:upload_link`, `fs:copy_file`, `fs:copy_folder`, `fs:delete_file`, `fs:delete_folder`, `fs:delete_from_trash`, `fs:move_file`, `fs:move_folder`, `fs:restore_from_trash`, `fs:lock_file`, `fs:unlock_file`, `fs:folder_project_activity` |
| Links (`link:*`) | `link:create`, `link:create_download_link`, `link:create_upload_link`, `link:delete`, `link:delete_download_link`, `link:delete_upload_link` |
| Comments (`comment:*`) | `comment:set_comment`, `comment:remove_comment` |
| Permissions (`permission:*`) | `permission:permission_change` |
| Metadata (`meta:*`) | `meta:add_metadata_key`, `meta:delete_metadata_key` |
| Workflows | `workflow:created`, `workflow:completed`, `workflow:approvaltask_approved`, `workflow:approvaltask_rejected` |
| Groups | `group:create`, `group:delete`, `group:rename`, `group:add_members`, `group:remove_members`, `group:add_owner`, `group:remove_owner` |

Egnyte lists further workflow events, such as `workflow:edited` and `workflow:cancelled`, as planned, and `user:create`, `user:delete`, `user:disable` and `user:enable` as implemented but not yet supported in v1.

A delivery looks like this. Egnyte's own example uses `...` for most values:

```json
{
  "data": [
    {
      "eventId": "...",
      "domain": "...",
      "timestamp": 1710000000000,
      "user": {
        "id": 1,
        "displayName": "...",
        "username": "...",
        "email": "..."
      },
      "actionSource": "...",
      "eventType": "fs:add_file",
      "webhookId": "...",
      "data": {}
    }
  ]
}

```

The common fields on every item:

| Field | Notes |
| --- | --- |
| `eventId` | Unique per Egnyte domain. Use it for deduplication |
| `domain` | The Egnyte domain where the event happened |
| `timestamp` | Unix time in milliseconds |
| `user` | `id`, `displayName`, `username`, `email`, plus optional `clientIdHash` (SHA-1 of the OAuth client ID that caused the event) and `impersonatedBy` |
| `actionSource` | The API or client used to perform the action |
| `eventType` | The most specific name for the event |
| `webhookId` | The registration that matched |
| `customProperties` | Optional, such as `PROJECT_ID` and `fileUpdated` |
| `data` | Event-specific fields |

The event-specific `data` varies by type. `fs:add_file` carries `targetEntryId`, `targetGroupId`, `targetFileChecksum`, `targetPath` and `itemType` (`file` or `version`). `fs:move_file` adds `sourcePath`. `permission:permission_change` carries `folderPath`, `newPrivilege`, `oldPrivilege` and `assignerId`, plus nullable `assignee`, `assigneeId`, `assigner` and `groupName`. Workflow events include a `workflow` object and a `firstEntity` object describing the file.

> **Inspect Egnyte webhooks as they arrive.** Register a webhook against a [Hookdeck Inspect](https://inspect.hookdeck.com) URL and upload a file to see a real `data` array, with no account or setup required.

## Setting up Egnyte webhooks

Egnyte documents webhook registration through the API. You need an OAuth access token for your Egnyte domain with the `Egnyte.webhooks` scope. Generate an `authHeader` value first, for example `Bearer ` followed by the output of `openssl rand -hex 32`.

```bash
curl -X POST "https://{domain}.egnyte.com/pubapi/v1/webhooks" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/egnyte",
    "eventType": ["fs:add_file", "fs:delete_file", "fs:move_file"],
    "path": "/Shared/Documents/Invoices",
    "authHeader": "Bearer your-long-random-token"
  }'

```

The `Authorization` header on this call is your OAuth token for the API. The `authHeader` in the body is the separate secret Egnyte will send back to your endpoint. A `201` response returns the `webhookId`, the `expires` timestamp and a `status` of `enabled`.

`authHeader` is optional in the API, but without it Egnyte sends nothing you can check, so always set it. To change the URL, events, paths or `authHeader` later, call `PUT /pubapi/v1/webhooks/{webhookId}` with the full configuration. `GET /pubapi/v1/webhooks/{webhookId}/details` returns the current settings, including `authHeader`.

**With Hookdeck.** Create a Hookdeck source with the **Egnyte** type and register its URL as the webhook `url`. The source has one field, **Authorization Header**: paste the `authHeader` value exactly as you registered it, including any `Bearer ` prefix. The source compares the whole header with that value and accepts `POST`.

For local development, the [Hookdeck CLI](/docs/cli) gives you a public HTTPS URL with a valid certificate: `hookdeck listen 3000 egnyte --path /webhooks/egnyte`.

## Securing Egnyte webhooks

Compare the whole `authorization` header with the `authHeader` you registered, in constant time. Egnyte sends the value verbatim, so don't strip a prefix or decode anything.

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

// Exactly the authHeader you registered, e.g. "Bearer 7c1e...".
const EXPECTED = process.env.EGNYTE_AUTHORIZATION;

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);
}

const app = express();

app.post('/webhooks/egnyte', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('authorization');
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

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

  for (const event of Array.isArray(body.data) ? body.data : []) {
    // Deduplicate on event.eventId, then route on event.eventType.
  }

  // Egnyte treats anything other than 200 as a failure.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

EXPECTED = os.environ.get("EGNYTE_AUTHORIZATION", "")

def verify_egnyte_request(headers) -> bool:
    if not EXPECTED:
        return False  # never accept when unconfigured
    received = headers.get("authorization")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))

```

A static header proves the sender knows the secret. It does not cover the body, and there is no signed timestamp, so a captured request can be replayed and still pass. Serve the endpoint over HTTPS only (Egnyte requires it anyway), use a long random `authHeader`, deduplicate on `eventId` so a replayed event does nothing, and rotate the value with `PUT` if it might have leaked.

> **Make Egnyte webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `authHeader` on every request, answers Egnyte with the `200` it expects, and queues every event durably so a slow or failing service never gets your webhook disabled.

## Egnyte webhook limitations and pain points

### Four failures and the webhook is disabled

**The Problem:** An outage of an hour or so, or a bad deploy that returns errors, doesn't just lose events. It switches the webhook off, and nothing more arrives until someone notices.

**Why It Happens:** Egnyte retries a failed delivery immediately, then after 1 minute, 10 minutes and 1 hour. After the fourth attempt the registration is disabled.

**Workarounds:**

* Return `200` as soon as the header checks out and process afterwards.
* Monitor `GET /pubapi/v1/webhooks/{webhookId}/status` and alert when it reads `disabled`.
* Re-enable with `POST /pubapi/v1/webhooks/{webhookId}/status`, then backfill the gap from the Events API.

**How Hookdeck Can Help:** Hookdeck answers Egnyte with a `200` as soon as it has stored the request, so your own outages no longer count against Egnyte's four attempts. Delivery to your service then uses [retries](/docs/retries) you configure, and failed events can be replayed once you've fixed the problem.

### Only `200` counts

**The Problem:** A handler that returns `202 Accepted` or `204 No Content` is treated as failing, and after four of those the webhook is disabled.

**Why It Happens:** Egnyte requires exactly HTTP `200`. Any other status schedules a retry.

**Workarounds:**

* Return exactly `200` from your webhook route, even for events you ignore.
* Don't return `4xx` for event types you don't handle. Narrow the `eventType` list instead.

**How Hookdeck Can Help:** Hookdeck returns `200` to Egnyte for every request it receives, so your service's status codes only affect Hookdeck's delivery to you.

### A wrong `authHeader` disables the webhook too

**The Problem:** A rotation done in the wrong order, or a missing `Bearer ` prefix on one side, makes your receiver return `401`, and four of those disable the registration.

**Why It Happens:** Egnyte counts a `401` like any other non-`200` response.

**Workarounds:**

* Rotate by accepting both the old and new values in your receiver, then calling `PUT` with the new `authHeader`, then removing the old value.
* Compare `GET .../details` against your stored value when deliveries start failing.

**How Hookdeck Can Help:** The Egnyte source answers with a `200` even when the header is wrong, so a mismatch doesn't disable the webhook. The request is recorded as failed verification and never reaches your service, which makes the mistake visible without losing the registration.

### Batched events in one request

**The Problem:** A handler that reads one event per request silently drops the rest.

**Why It Happens:** The body is `{"data": [...]}`, and one delivery can carry several events, each with its own `eventType`.

**Workarounds:**

* Always loop over `data`.
* Deduplicate and process each item on its own, so one bad item doesn't fail the batch.

**How Hookdeck Can Help:** Hookdeck [transformations](/docs/transformations) can reshape the batch before it reaches your service, and [filters](/docs/filters) can route deliveries by `eventType` to different destinations.

### Feedback loops from your own changes

**The Problem:** An integration that writes files back into Egnyte receives webhooks about its own writes, and can end up reacting to itself.

**Why It Happens:** Every change in a watched path produces an event, whoever made it.

**Workarounds:**

* Compare `user.clientIdHash`, a SHA-1 hash of the OAuth client ID that caused the event, with the hash of your own client ID, and skip matches.
* Narrow `path` to the folders you actually need.

**How Hookdeck Can Help:** A Hookdeck [transformation](/docs/transformations) can remove the items whose `clientIdHash` matches your application before the batch is delivered.

## Best practices

### Subscribe narrowly

Name the event types and paths you need. Egnyte itself advises against omitting `eventType`, and a narrow subscription means less traffic and fewer events your handler has to ignore.

### Deduplicate on `eventId`

Retries resend the same events, so store each `eventId` before acting and skip ones you've seen. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Monitor each registration's status

Track each webhook's `status` and `expires` values. A disabled or expired registration fails silently from your side: your endpoint is healthy but nothing arrives.

### Keep the API token and the `authHeader` separate

The OAuth token that manages webhooks and the `authHeader` Egnyte sends to you are different secrets. Never reuse the access token as the `authHeader`.

### Acknowledge fast and process asynchronously

Verify the header, store the batch, return `200`, and process each event in a background job. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Egnyte sends batches of file, link, comment, permission, metadata, workflow and group events as JSON `POST` requests, authenticated by the `authHeader` you register and nothing else. Always set `authHeader`, compare it whole in constant time over HTTPS, loop over `data`, deduplicate on `eventId`, and return exactly `200`, because four failed attempts disable the webhook.

[Hookdeck Event Gateway](https://hookdeck.com) checks the header, answers Egnyte immediately, and keeps every event until your service has processed it, with retries and replay on your terms.

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