# Guide to Datto RMM Webhooks: Features and Best Practices

Datto RMM webhooks send monitor alerts from Datto's remote monitoring and management platform to your own systems. When an alert is raised on a managed device, and optionally when it resolves, Datto RMM can send a notification you shape yourself, so you can open tickets in a PSA Datto doesn't integrate with, post to an on-call tool, or keep your own alert history.

This guide covers how Global Webhooks are set up, how to authenticate their requests with a secret header, how the alert templates and variables work, and the practices that make a Datto RMM receiver dependable.

## What are Datto RMM webhooks?

Datto RMM can send a webhook from a single monitor, or from every monitor in the account through **Global Webhooks**. This guide covers Global Webhooks only. Turning them on disables the webhooks set on individual monitors, and per-monitor webhooks have no way to carry a secret header, so Global Webhooks are the option you can authenticate.

The body is a template you write. Datto RMM replaces `[variable]` placeholders with alert, device and site values, and you can write a second template for when the alert resolves. Datto RMM adds no event field of its own, so the template decides everything your endpoint sees.

Datto RMM doesn't sign webhooks. The way to tell a real request from a forged one is a header you add with **Add Header** and check on your side.

## Datto RMM webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Setup** > **Integrations** > **Global Webhooks** |
| Permissions | Administrator |
| Scope | Alerts of the priorities you select, from all monitors |
| URL | Up to 500 characters |
| Content Type | `application/json` (default), `text/plain` or `application/x-www-form-urlencoded` |
| Body | Your alert raised template, plus an optional alert resolved template |
| Custom headers | **Add Header**, name and value, with an option to hide the value |
| Maximum payload | 64 KB |
| Signature and timestamp | None |
| Test | **Test Alert Webhook**, raised or resolved |
| History | Sent messages appear on the alert's **Timeline** card |
| Method, retries, timeout, source IPs | Not documented |

## Alerts, templates and variables

Global Webhooks send two kinds of notification:

* **Alert raised**, sent when a monitor raises an alert of a priority you selected.
* **Alert resolved**, sent only if you fill in the resolved template. It goes to the same URL, and only when the alert resolves automatically. Resolving an alert by hand sends nothing.

Because there is no event field, write one into each template. This is an illustration, not a vendor schema:

```json
{
  "status": "raised",
  "alert_uid": "[alert_uid]",
  "alert_type": "[alert_type]",
  "alert_category": "[alert_category]",
  "alert_message": "[alert_message]",
  "device_hostname": "[device_hostname]",
  "site_name": "[site_name]"
}

```

Use `"status": "resolved"` in the resolved template, and keep `[alert_uid]` in both so you can match a resolution to the alert it closes.

The variables Datto documents include:

| Group | Variables |
| --- | --- |
| Alert | `[alert_uid]`, `[alert_type]`, `[alert_category]`, `[alert]`, `[alert_message]` (also `_en`, `_de`, `_fr`) |
| Device | `[device_id]`, `[device_uid]`, `[device_hostname]` (or `[hostname]`), `[device_os]`, `[device_description]`, `[device_ip]`, `[device_udf1]` to `[device_udf30]`, `[last_user]` |
| Site | `[site_id]`, `[site_uid]`, `[site_name]` (or `[sitename]`) |
| Other | `[platform]` |

`[device_ip]` is the device's public IP address, not its LAN address. Datto's docs don't say whether substituted values are escaped for JSON, so an alert message containing a quote could break the body. Test with real alerts and handle a body that doesn't parse.

> **Inspect Datto RMM webhooks as they arrive.** Point the Global Webhook at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL and click **Test Alert Webhook** to see exactly what your template renders, with no account or setup required.

## Setting up Datto RMM 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`.

1. Sign in as an Administrator and go to **Setup** > **Integrations** > **Global Webhooks**. Turning these on disables your per-monitor webhooks, so plan for anything that depends on them.
2. Select the alert priorities to send.
3. Enter your HTTPS endpoint as the URL.
4. Keep **Content Type** at `application/json` unless your receiver needs something else.
5. Click **Add Header**, enter your header's name and value, and hide the value.
6. Write the alert raised template, and the alert resolved template if you want resolutions.
7. Save, then click **Test Alert Webhook** for both raised and resolved.

To route through Hookdeck, create a source with the **Datto RMM** type and use its URL as the webhook URL. Enter the header's name in **API Key Header** and its exact value, including any `Bearer ` prefix, in **API Key**. The source accepts `POST`. Datto doesn't document which method Global Webhooks use, so send a test alert and check that it arrives before you rely on it.

## Securing Datto RMM webhooks

Read the header you added, case-insensitively, and compare its whole value with the one you stored, in constant time. Check the header before parsing the body, and don't assume JSON: the content type is whatever you set on the webhook.

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

const HEADER = (process.env.DATTO_RMM_WEBHOOK_HEADER || 'Authorization').toLowerCase();
const SECRET = process.env.DATTO_RMM_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);
}

const app = express();

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

  const raw = req.body.toString('utf8');
  let alert = raw;
  if (req.is('application/json')) {
    try {
      alert = JSON.parse(raw);
    } catch {
      return res.sendStatus(400); // usually a template or escaping problem
    }
  }

  // With the template above: alert.status is "raised" or "resolved".
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

HEADER = os.environ.get("DATTO_RMM_WEBHOOK_HEADER", "Authorization")
SECRET = os.environ.get("DATTO_RMM_WEBHOOK_SECRET", "")

def verify_datto_rmm_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. Serve the endpoint over HTTPS only, use a long random value, and replace it in Datto RMM and in your receiver if it might have leaked.

> **Make Datto RMM alerts production-ready.** [Hookdeck Event Gateway](/event-gateway) checks your secret header on every Datto RMM alert, queues each one durably, and lets you replay any alert after a fix.

## Datto RMM webhook limitations and pain points

### No signature, and per-monitor webhooks can't be authenticated

**The Problem:** Anyone who learns the header value can send convincing alerts, and alerts from per-monitor webhooks arrive with nothing to check.

**Why It Happens:** Datto RMM computes nothing over the body, and only Global Webhooks let you add a header.

**Workarounds:**

* Use Global Webhooks with a long random header value, over HTTPS only.
* Before acting on an alert, such as running a remediation job, confirm it in Datto RMM.

**How Hookdeck Can Help:** Hookdeck's Datto RMM source compares the header you name with the exact value you enter. Hookdeck answers Datto RMM with a `200` either way, but a request with a missing or wrong value is recorded as failed verification and never reaches your destination.

### Manual resolutions send nothing

**The Problem:** An alert stays open in your ticketing system after a technician has closed it in Datto RMM.

**Why It Happens:** The resolved payload is only sent when an alert auto-resolves.

**Workarounds:**

* Reconcile your open tickets against Datto RMM's open alerts on a schedule.
* Let technicians close the downstream ticket directly when they resolve by hand.

**How Hookdeck Can Help:** Hookdeck can't create an event Datto doesn't send, but its request log shows which alerts did receive a resolution, which narrows the reconciliation.

### Delivery behaviour is undocumented

**The Problem:** You can't tell whether an alert sent during your outage will arrive later.

**Why It Happens:** Datto's docs describe the Timeline card but not a timeout or retry policy.

**Workarounds:**

* Respond with `200` as soon as the header checks out, and do the work afterwards.
* After an outage, check the Timeline card of affected alerts.

**How Hookdeck Can Help:** Hookdeck accepts each alert as soon as it arrives, stores it, and delivers it to your service with [retries](/docs/retries) you configure, so a short outage on your side doesn't depend on Datto retrying.

### Template mistakes break the body

**The Problem:** One alert produces JSON your parser rejects, or a template edit renames a field.

**Why It Happens:** You own the template, Datto doesn't validate it, and the docs don't say whether values are escaped.

**Workarounds:**

* Keep the templates in version control and change them alongside your receiver.
* Return `400` for an unparseable body and log the raw request.

**How Hookdeck Can Help:** Hookdeck stores every request with its raw body and headers, and Hookdeck [transformations](/docs/transformations) can normalise the body before delivery. Once the template is fixed, replay the failed alerts.

## Best practices

### Store the full header value

Keep the exact value, prefix included, as one string and compare it whole. Put the name and value in configuration, not code, so you can rotate them.

### Write a status field and keep the alert UID

Put your own `status` in each template, and include `[alert_uid]` in both, so the receiver can route and match resolutions to alerts.

### Deduplicate on the alert UID and status

Use `[alert_uid]` plus your status field as an idempotency key, and store it before acting. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Stay under 64 KB

Long alert messages and many user-defined fields add up. Send identifiers and the fields you act on, rather than every variable.

### Acknowledge fast and process asynchronously

Check the header, store the alert, return `200`, and create tickets afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Datto RMM Global Webhooks send each alert, and each automatic resolution, as a body built from your own template, authenticated only by a header you add. Use Global Webhooks rather than per-monitor ones, compare the whole header value in constant time, write a status field and the alert UID into both templates, and reconcile manual resolutions separately.

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

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