# Guide to Bitdefender Webhooks: Features and Best Practices

Bitdefender webhooks bring endpoint security events out of GravityZone as they happen: malware detected and quarantined, a phishing page blocked, a firewall rule triggered, ransomware activity stopped, an incident opened or reassigned, an agent installed or removed. GravityZone calls this the **Event Push Service**, and it is how security teams feed detections into a SIEM, a ticketing system or their own automation without polling the GravityZone API.

This guide covers how to turn the Event Push Service on for a generic HTTP collector, the JSON-RPC message format and event modules, how to authenticate pushes with the `Authorization` value you set, and the network and delivery details that trip up new collectors.

## What are Bitdefender webhooks?

The Event Push Service sends GravityZone events to a web service you run. You configure it with the `setPushEventSettings` method of the GravityZone public API, choosing a service type (`jsonRPC` for your own HTTP collector, or one of the SIEM-specific types), the URL, an authorization value, and the event types you want.

With `jsonRPC`, every push is a JSON-RPC 2.0 call to a method named `addEvents`. Its `params.events` array holds one or more events, and each event's `module` field says what kind it is: `av` for an antimalware detection, `fw` for a firewall event, `new-incident` for an incident, and so on.

Bitdefender doesn't sign pushes with a shared secret you can check against a standard scheme. The `authorization` string you set in `serviceSettings` is sent as the `Authorization` header of every request, exactly as you entered it. Your collector compares it with the stored value.

## Bitdefender webhook features

| Feature | Details |
| --- | --- |
| Configuration | `setPushEventSettings` in the GravityZone public API, one push configuration per company |
| Service type | `jsonRPC` for your own collector. Also `splunk`, `cef`, `qradar`, `azureSentinel`, `azureSentinelV2` |
| Message | JSON-RPC 2.0, `method: "addEvents"`, `params.events` array |
| Event identifier | `module` on each event |
| Authentication | `serviceSettings.authorization`, sent verbatim as `Authorization` |
| Body digest | `Event-Push-Service-Md5` header (MD5-based, see below) |
| Transport | HTTPS with TLS 1.2 or higher |
| Certificate check | `requireValidSslCertificate` |
| Source IPs | 25 published GravityZone addresses, which the collector must allow |
| Acknowledgement | Any `2xx`. Anything else, or no response, is an error |
| Testing and monitoring | `sendTestPushEvent`, `getPushEventStats`, `resetPushEventStats` |
| Retries and timeout | Not documented |

## Common events

| `module` | Event |
| --- | --- |
| `av` | Antimalware detection |
| `aph` | Antiphishing |
| `avc` | Advanced Threat Control |
| `hd` | Hyper Detect |
| `fw` | Firewall |
| `dp` | Data Protection |
| `uc` | User Control and Content Control |
| `network-sandboxing` | Sandbox Analyzer detection |
| `antiexploit` | Antiexploit event |
| `network-monitor` | Network Attack Defense event |
| `ransomware-mitigation` | Ransomware activity detection |
| `new-incident`, `new-extended-incident` | A new incident |
| `incident-status-changed`, `incident-assignee-changed`, `incident-priority-changed` | An incident changes |
| `incident-note-created`, `incident-note-updated`, `incident-note-deleted` | Incident notes |
| `install`, `uninstall`, `hwid-change` | Agent lifecycle |
| `endpoint-moved-in`, `endpoint-moved-out` | An endpoint is moved in or moved out |
| `modules`, `registration`, `task-status`, `troubleshooting-activity` | Product and task status |
| `sva`, `sva-load`, `supa-update-status`, `security-container-update-available` | Security Server and update status |
| `exchange-malware`, `exchange-user-credentials`, `exchange-organization-info` | Exchange protection |
| `adcloud` | Cloud Active Directory integration |

Bitdefender's Event Types reference lists a few more, such as `partner-changed` and `integrations-hub-status`. Here is the documented antiphishing push:

```json
{
  "jsonrpc": "2.0",
  "method": "addEvents",
  "params": {
    "events": [
      {
        "companyId": "59a14b271da197c6108b4567",
        "computer_name": "FC-EXCHANGE-01",
        "computer_fqdn": "fc-exchange-01.fc.dom",
        "computer_ip": "192.168.0.1",
        "computer_id": "59b7d9bfa849af3a1465b7e4",
        "product_installed": "BEST",
        "aph_type": "phishing",
        "url": "http://example.com/account/support/",
        "status": "aph_blocked",
        "last_blocked": "2017-09-14T08:49:43.000Z",
        "count": 1,
        "module": "aph"
      }
    ]
  },
  "id": 1505378984190
}

```

The outer `method` is always `addEvents`, so it names the envelope, not the event. Route on each event's `module`, and expect one push to hold several events. Most endpoint events also carry `companyId`, `computer_name` and `computer_id`.

> **Inspect Bitdefender webhooks as they arrive.** Point the Event Push Service at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL and call `sendTestPushEvent` to see the exact JSON-RPC body and headers GravityZone sends, with no account or setup required.

## Setting up Bitdefender webhooks

Choose the full `Authorization` value first, for example `Bearer ` followed by the output of `openssl rand -hex 32`.

Then call `setPushEventSettings` with your GravityZone API key:

```json
{
  "jsonrpc": "2.0",
  "method": "setPushEventSettings",
  "id": "ad12cb61-52b3-4209-a87a-93a8530d91cb",
  "params": {
    "status": 1,
    "serviceType": "jsonRPC",
    "serviceSettings": {
      "url": "https://example.com/webhooks/bitdefender",
      "authorization": "Bearer YOUR_LONG_RANDOM_VALUE",
      "requireValidSslCertificate": true
    },
    "subscribeToEventTypes": {
      "av": true,
      "aph": true,
      "fw": true,
      "new-incident": true,
      "incident-status-changed": true
    }
  }
}

```

`status` is `1` to enable the service and `0` to disable it. Subscribe only to the modules you will handle. Use `getPushEventSettings` to read the configuration back and `sendTestPushEvent` to send a test.

Your collector must accept HTTPS with TLS 1.2 or higher and allow GravityZone's egress IPs, which Bitdefender lists on the `setPushEventSettings` page. Keep `requireValidSslCertificate` set to `true` in production.

To route through Hookdeck, create a source with the **Bitdefender** type and use its URL as `serviceSettings.url`. The source has one field, **Authorization Header**. As Hookdeck's setup note says, paste the authorization value exactly as set in GravityZone, including any `Bearer ` or `Basic ` prefix. Hookdeck compares the whole header with it and accepts `POST`.

## Securing Bitdefender webhooks

Compare the whole `Authorization` header with the value you set in `serviceSettings.authorization`, in constant time. Don't strip the prefix or decode it.

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

// Exactly what you set in serviceSettings.authorization, e.g. "Bearer 3f9c...".
const EXPECTED = process.env.BITDEFENDER_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/bitdefender', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('authorization');
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

  let call;
  try {
    call = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (call.method !== 'addEvents' || !Array.isArray(call.params?.events)) {
    return res.sendStatus(400);
  }

  // Queue each event; a worker routes on event.module.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

EXPECTED = os.environ.get("BITDEFENDER_AUTHORIZATION", "")  # e.g. "Bearer 3f9c..."

def verify_bitdefender_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 `Authorization` value authenticates the sender, not the body, and with no timestamp a captured push can be replayed and still pass. Use HTTPS (GravityZone requires TLS 1.2 or higher anyway), a long random value, and the published IP allowlist as a second check. Rotate the value with `setPushEventSettings` if it might have leaked.

Bitdefender also documents an `Event-Push-Service-Md5` header, described as `md5(api_key, md5(message_body))`; its PHP example concatenates the API key with the hex MD5 of the raw body and hashes the result. That ties the header to the body, but it relies on MD5, the page doesn't say exactly which key is meant, and its sample uses two different variable names for the key. If you want to use it, confirm it against a captured push first. Hookdeck's Bitdefender source doesn't check it.

> **Make Bitdefender webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `Authorization` value, answers GravityZone with a `2xx` as soon as a push arrives, and queues every detection durably for your SIEM or automation.

## Bitdefender webhook limitations and pain points

### A collector GravityZone can't reach

**The Problem:** Pushes never arrive, or `getPushEventStats` shows errors, even though the collector works from your own machine.

**Why It Happens:** GravityZone sends only from its published IP addresses, requires TLS 1.2 or higher, and validates certificates when `requireValidSslCertificate` is `true`.

**Workarounds:**

* Allow every listed GravityZone IP at your firewall and load balancer.
* Serve the collector with a valid certificate on a public address.
* Check `getPushEventStats` after `sendTestPushEvent`.

**How Hookdeck Can Help:** A Hookdeck source URL is public, serves a valid certificate and supports modern TLS, so GravityZone can reach it without firewall changes on your side. Hookdeck then delivers to your collector over the network path you control.

### Undocumented retry behaviour

**The Problem:** A detection pushed while your collector was down may never arrive.

**Why It Happens:** Bitdefender documents that anything other than a `2xx`, or no response, counts as an error, but not whether or when a failed push is retried.

**Workarounds:**

* Return a `2xx` as soon as the push is stored, and process it afterwards.
* For anything critical, cross-check against GravityZone reports or the API.

**How Hookdeck Can Help:** Hookdeck stores each push as it arrives and delivers it to your collector with [retries](/docs/retries) on your schedule, and you can replay any push after a fix.

### Batches of mixed events

**The Problem:** A handler written for one event per request misses detections, or fails a whole batch on one unexpected module.

**Why It Happens:** `params.events` is an array, and one push can mix modules.

**Workarounds:**

* Iterate over every event and route each on its `module`.
* Acknowledge modules you don't handle instead of rejecting the push.

**How Hookdeck Can Help:** A [transformation](/docs/transformations) can reshape a push before delivery, and the original request stays in Hookdeck for replay.

### The prefix belongs to the value

**The Problem:** Every push fails your check because you stored the token without `Bearer `, or decoded a `Basic ` value.

**Why It Happens:** GravityZone sends the `authorization` string exactly as you set it, prefix and all.

**Workarounds:**

* Store the full string you sent in `setPushEventSettings` and compare it whole.

**How Hookdeck Can Help:** Hookdeck's Bitdefender source compares the whole `Authorization` value you paste. Hookdeck answers GravityZone with a `200` either way, but a push with a missing or wrong value is recorded as failed verification and never reaches your collector.

## Best practices

### Subscribe to what you handle

`subscribeToEventTypes` takes one flag per module. Turn on only the modules your collector processes, so incident and agent noise doesn't hide detections.

### Deduplicate on event content

GravityZone events have no single documented event ID. Build a key from `module`, `computer_id` and the event's own timestamp fields where present. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Keep certificate validation on

Set `requireValidSslCertificate` to `true` outside test environments.

### Monitor push statistics

Call `getPushEventStats` regularly, or alert on it, so a failing collector doesn't go unnoticed.

### Acknowledge fast and process asynchronously

Store the push and return a `2xx`, then enrich and forward the events in the background. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

The GravityZone Event Push Service sends batches of security events as JSON-RPC `addEvents` calls, authenticated by the `Authorization` value you set with `setPushEventSettings`. Compare that whole value in constant time, route each event on its `module`, allow GravityZone's IPs over TLS 1.2 or higher, and return a `2xx` quickly, because Bitdefender doesn't document what happens to a push you fail.

[Hookdeck Event Gateway](https://hookdeck.com) checks the header, stores every push as it arrives, and delivers it to your collector with retries and replay.

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