> For the complete documentation index, see [llms.txt](https://docs.powermonitor.com.br/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.powermonitor.com.br/en/power-monitor/configuracoes/webhooks-e-api-publica.md).

# Webhooks and public API (technical reference)

Technical reference for Power Monitor integrations: webhook format, HMAC signature verification, delivery behavior and the read-only public API with API keys.

This page is the reference for those who will **implement** the integrations configured in [Settings › Alerts](/en/power-monitor/configuracoes/alertas.md): receiving the generic webhook, validating the signature and consuming the public API. For the step-by-step in the interface (creating endpoints, generating keys, testing), see the [Settings › Alerts](/en/power-monitor/configuracoes/alertas.md) page.

{% hint style="warning" %}
The examples on this page use **fictitious** values (keys, addresses and identifiers). Never publish the signing key or the API key in repositories, e-mails or shared reports.
{% endhint %}

## Generic webhook

The **Generic webhook** endpoint sends a `POST` with a JSON body (`Content-Type: application/json`) to the registered URL, once for each selected alert event.

### Events

| Event (`event`)      | When it is sent                                                              |
| -------------------- | ---------------------------------------------------------------------------- |
| `alert.opened`       | An incident opens.                                                           |
| `alert.recovered`    | The resource recovers (or the incident is marked as resolved).               |
| `alert.escalated`    | The incident stayed unacknowledged beyond the time of the escalation policy. |
| `alert.acknowledged` | Someone acknowledged the incident.                                           |

Reoccurrences of an already open incident do not generate a new event. Alerts that are silenced or correlated to an open cause are not sent.

### Request body

```json
{
  "id": "6f0c1f0e-0000-0000-0000-000000000000",
  "event": "alert.opened",
  "occurredAt": "2026-10-05T12:00:00Z",
  "alert": {
    "id": "b1c2d3e4-0000-0000-0000-000000000000",
    "title": "Capacity F64",
    "message": "Alert message (up to 1000 characters)",
    "sourceType": "Capacity",
    "sourceId": "00000000-0000-0000-0000-000000000000",
    "workspaceName": null,
    "capacityName": "Capacity F64",
    "severity": "error",
    "status": "open",
    "acknowledged": false,
    "openedAt": "2026-10-05T12:00:00Z",
    "resolvedAt": null,
    "failureCount": 1,
    "url": "https://<your-power-monitor-address>/monitoring/alert"
  }
}
```

* `id` is the identifier of the **delivery** (use it to avoid processing the same event twice).
* `severity` is `error` or `info`; `status` is `open` or `resolved`.
* `sourceType` is the type of the source (for example `Gateway`, `Capacity`, `Dataset`, `FabricJob`, `Mirroring`, `DataFreshness`, `Connector`).
* In a **test** send, the body also carries `"test": true`.
* The body does not include user e-mails or internal organization identifiers.

### Headers

| Header                     | Content                                                           |
| -------------------------- | ----------------------------------------------------------------- |
| `X-PowerMonitor-Event`     | Event name (for example `alert.opened`).                          |
| `X-PowerMonitor-Delivery`  | Delivery identifier.                                              |
| `Idempotency-Key`          | The same delivery identifier. Use it to deduplicate.              |
| `X-PowerMonitor-Timestamp` | Moment of sending, in Unix seconds. It changes with each attempt. |
| `X-PowerMonitor-Signature` | `sha256=` followed by the HMAC-SHA256 in lowercase hexadecimal.   |
| `User-Agent`               | `PowerMonitor-Webhook/1.0`.                                       |

Reply with any **2xx** status to confirm the delivery.

### How the signature is calculated

```
signature = "sha256=" + lowercase_hex( HMAC_SHA256( signing_key, timestamp + "." + raw_body ) )
```

* `signing_key` is the key shown only once when the endpoint is created (format `whsec_...`). Use it as text, exactly as it was shown.
* `timestamp` is the value of the `X-PowerMonitor-Timestamp` header.
* `raw_body` is the request body **exactly as received**, before any `JSON.parse`. Reserializing the JSON changes the bytes and invalidates the signature.

### How to verify (example)

Recommended steps on the receiver: read the raw body, recompute the signature, compare in **constant time**, reject timestamps outside a tolerance window (300 seconds) and deduplicate by `Idempotency-Key`.

**Node.js (Express)**

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

const SIGNING_SECRET = process.env.PM_SIGNING_SECRET; // e.g. whsec_EXAMPLE_NOT_REAL
const app = express();

app.post('/hooks/power-monitor', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-PowerMonitor-Timestamp');
  const received = req.get('X-PowerMonitor-Signature') || '';
  const body = req.body.toString('utf8'); // raw body

  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);

  const expected = 'sha256=' + crypto
    .createHmac('sha256', SIGNING_SECRET)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  const deliveryId = req.get('Idempotency-Key'); // deduplicate by this value
  const event = JSON.parse(body);
  // ... process the event ...
  res.sendStatus(200);
});

app.listen(3000);
```

**Python**

```python
import hashlib, hmac, time

def is_valid(signing_secret: str, timestamp: str, raw_body: bytes, received_signature: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    material = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(signing_secret.encode(), material, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received_signature)
```

### Delivery, retries and limits

* **Time limit:** 10 seconds per call.
* **Retries:** up to 5 attempts in total, waiting 1 minute, 5 minutes, 30 minutes and 2 hours between them. Failures with no response (timeout, network) and responses 408, 429 and 5xx are retried. 4xx responses (except 408 and 429) abandon the delivery.
* **Processing:** pending deliveries are processed continuously (cycles of about 1 minute), with a cap per organization and per cycle so that a very noisy organization does not monopolize sending. During peaks, delivery may be delayed.
* **Order:** there is no guarantee of order between events; use `occurredAt` and `alert.status` to decide the final state.
* **History:** the **Deliveries** button of the endpoint shows the result of each delivery for 30 days.
* **Destination security (SSRF protection):** only **https** URLs of public hosts, without embedded credentials, are accepted. Hosts such as `localhost`, names ending in `.internal` or `.local`, loopback addresses, private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local (169.254.0.0/16, which includes the cloud metadata service) and equivalent IPv6 are refused, and the address is checked again at connection time. Redirects (3xx) are **not** followed and Power Monitor does not use a proxy for these calls. The connection failure message is intentionally generic.
* **Limit:** 20 endpoints per organization.

## ITSM connectors

The **PagerDuty**, **Opsgenie**, **ServiceNow** and **Azure DevOps** types use the same delivery mechanism (same events, filters, retries and protections), but speak each product's format. The alert identifier in Power Monitor is used to group the events of the same incident.

| Destination                   | Opened                                                                        | Acknowledged                        | Escalated                                                    | Recovered                                                                                                                                      |
| ----------------------------- | ----------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **PagerDuty** (Events API v2) | Triggers an event (`trigger`) with a deduplication key equal to the alert id. | `acknowledge` on the same key.      | New `trigger` marked `[ESCALADO]`, with `critical` severity. | `resolve` on the same key.                                                                                                                     |
| **Opsgenie**                  | Creates the alert (alias = alert id).                                         | Acknowledges the alert.             | Adds a note.                                                 | Closes the alert.                                                                                                                              |
| **ServiceNow** (Table API)    | Creates an incident.                                                          | Updates to "In progress" (state 2). | Urgency 1 and a work note.                                   | Updates to "Resolved" (state 6).                                                                                                               |
| **Azure DevOps**              | Creates a Task work item in the given project, with the PowerMonitor tag.     | Adds a comment to the work item.    | Adds a comment to the work item.                             | Adds a comment and tries to move the work item to a completion state (if the project's process does not accept it, only the comment is added). |

Notes:

* If the opening event has not yet been delivered, the following events of the same incident wait and are retried (the external system needs the reference created at opening).
* The **Test** button opens a sample incident and closes it right after. If closing fails, the screen asks you to close it manually.
* Credentials (routing key, API key, token, username and password, PAT) are stored encrypted and are never returned by the interface.
* Message texts sent to ITSM tools are in Portuguese.

## Public API

The public API is **read-only** (`GET` only) and lives under the path `/api/public/v1`, at the same address as your Power Monitor. It exists to bring data to Power BI reports and scripts, using the [API keys](/en/power-monitor/configuracoes/alertas.md#api-keys) created by an administrator.

### Authentication

Send the key in one of the headers:

```
X-Api-Key: <YOUR_API_KEY>
Authorization: ApiKey <YOUR_API_KEY>
```

* The key identifies the **organization**: all queries return data of the key's organization. There is no per-user or workspace-scope filter.
* Each key only accesses the endpoints of the scopes it received. Without the required scope, the response is `403`.
* Missing, invalid, expired or revoked key: `401` (the message is the same in all four cases). Suspended organization: `403`.
* Many attempts with an invalid key from the same address (more than 10 per minute) start to receive `429`.
* **Usage limit:** 60 requests per minute per key (`429` when exceeded).
* Responses are not cached (`Cache-Control: no-store`).

### Response format

* JSON in camelCase, dates and times in ISO 8601 UTC, enumerations as text.
* Lists are paginated and return the envelope:

```json
{
  "value": [ ],
  "page": 1,
  "pageSize": 100,
  "totalCount": 250,
  "nextLink": "/api/public/v1/alerts?page=2&pageSize=100"
}
```

* `page` starts at 1 (maximum 1000) and `pageSize` goes from 1 to 500 (default 100). The `nextLink` is **relative**: prefix it with the address of your Power Monitor. Repeat the call until you receive an empty `nextLink`.
* Add `format=csv` to any list to receive the same page as a CSV file (UTF-8 with BOM). The total and the paging come in the `X-Total-Count`, `X-Page`, `X-Page-Size` and `X-Next-Link` headers.
* Errors have the format `{ "error": { "code": "...", "message": "..." } }`, with the codes `invalid_request` (400), `unauthorized` (401) and `forbidden` (403).

### Endpoints

| Endpoint                            | Scope                         | Parameters                                                                                                                                                                             | What it returns                                                                                                                                                                                                            |
| ----------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/public/v1/alerts`         | Alerts (`ReadAlerts`)         | `from`, `to` (ISO 8601 UTC; `from` inclusive, `to` exclusive; default: last 7 days; maximum 366 days), `status` (`open` or `resolved`), `sourceType`, `page`, `pageSize`, `format=csv` | One alert per incident: `sourceType`, `sourceName`, `severity`, `notificationKind`, `status`, `message`, `createdAt`, `resolvedAt`, `acknowledgedAt`, `failureCount`, `lastOccurrenceAt`, `workspaceName`, `capacityName`. |
| `GET /api/public/v1/alerts/metrics` | Alert metrics (`ReadMetrics`) | `from`, `to` (`yyyy-MM-dd`, inclusive days; default: last 7 days; maximum 366)                                                                                                         | Totals of the period: incidents, acknowledged, resolved, open and unacknowledged, silenced, correlated, reoccurrences, MTTA and MTTR (median, mean and samples), incidents by source type and noisiest sources.            |
| `GET /api/public/v1/capacities`     | Capacities (`ReadCapacities`) | `page`, `pageSize`, `format=csv`                                                                                                                                                       | `name`, `sku`, `state`, `region`.                                                                                                                                                                                          |
| `GET /api/public/v1/workspaces`     | Workspaces (`ReadWorkspaces`) | `page`, `pageSize`, `format=csv`                                                                                                                                                       | `name`, `remoteId`, `capacityName`.                                                                                                                                                                                        |
| `GET /api/public/v1/costs/daily`    | Costs (`ReadCosts`)           | `from`, `to` (`yyyy-MM-dd`, inclusive UTC days; default: last 30 complete days), `currency` (ISO 4217), `page`, `pageSize`, `format=csv`                                               | Daily cost per capacity: `date`, `capacityName`, `currency`, `cost`, `amortizedCost`, `overageCost`. A single currency per response (the one given or the predominant one in the period).                                  |
| `GET /api/public/v1/sla`            | SLA (`ReadSla`)               | `month` (`yyyy-MM`; default: previous month; up to 12 months back)                                                                                                                     | Monthly SLA report: totals with targets and compliance percentages, MTTA and MTTR, incidents by type, noisiest sources and availability per capacity.                                                                      |

### Examples

**curl**

```bash
curl -H "X-Api-Key: <YOUR_API_KEY>" \
  "https://<your-power-monitor-address>/api/public/v1/alerts?status=open&pageSize=100"
```

**Power BI (Power Query)**

```
let
    Response = Json.Document(Web.Contents("https://<your-power-monitor-address>/api/public/v1/alerts", [
        Headers = [#"X-Api-Key" = "<YOUR_API_KEY>"],
        Query = [status = "open", page = "1", pageSize = "500"]
    ])),
    Items = Table.FromRecords(Response[value])
in
    Items
```

For more than one page, call again varying the `page` parameter.

## Related pages

* [Settings › Alerts](/en/power-monitor/configuracoes/alertas.md): how to create endpoints and keys in the interface.
* [Monitoring › Alerts](/en/power-monitor/monitoramento/alertas.md): acknowledgement, silencing, escalation and metrics.
* [SLA Report](/en/power-monitor/monitoramento/relatorio-de-sla.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.powermonitor.com.br/en/power-monitor/configuracoes/webhooks-e-api-publica.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
