---
updatedAt: 2025-05-19T20:47:25.000Z
agentTools:
  projectIndex: https://docs.metaplane.dev/llms.txt
---

# Webhooks

Metaplane supports sending incident alerts to 3rd party systems via webhooks.

## Setup

First, navigate to the [alerts settings page](https://app.metaplane.dev/settings/alerts). Scroll to the bottom of the page and find the option to add a new webhook alert destination.

![](https://files.readme.io/1883b1b-image.png)

Fill out the form and then click "Add destination."

## Verify

![](https://files.readme.io/c9118f7-image.png)

You can have Metaplane send you a sample incident by using the actions menu to "Verify" the webhook alert destination.

Refer to the [alert routing docs](https://docs.metaplane.dev/docs/alert-routing) to learn more about how to route specific incidents to your webhook.

## Secure

If you'd like to secure your webhook by verifying that request come from Metaplane, you can enable request signatures.

![](https://files.readme.io/017db6cd66ac745d1a5c5d37574f81de3632fa73445f6d4fb357d506ad76ae2e-image.png)

![](https://files.readme.io/9c50efcaf65a218e04d80b7c87817b22e3efaa65cf383eed2b2f624585db5a7d-image.png)

![](https://files.readme.io/17acee278ab7f92c4471119b41fe7e0b8c16cb12380759005461986d4fbc7846-image.png)

If you lose your secret key before you're able to instrument request verification you can always generate a new key from the Secure dialog.

See the code examples below (Python and NodeJS) to understand how you should verify the webhook request.

```python Python
from flask import Flask, request, jsonify
import hmac
import hashlib

app = Flask(__name__)

WEBHOOK_SECRET = "YOUR_SECRET_HERE"

def verify_signature(payload, signature):
    expected_sig = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(signature, expected_sig)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    # Get signature from header
    signature = request.headers.get('x-metaplane-sig')
    if not signature:
        return jsonify({'error': 'No signature provided'}), 401
    
    # Get raw request body
    payload = request.get_data()
    
    # Verify signature
    if not verify_signature(payload, signature):
        return jsonify({'error': 'Invalid signature'}), 401
    
    # If signature is valid, process the webhook
    data = request.json
    print(f"Received verified webhook: {data}")
    
    return jsonify({'status': 'success'}), 200

if __name__ == '__main__':
    app.run(port=3000)
```
```javascript NodeJS
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.raw({ type: 'application/json' })); // Important: raw body for signature verification

const WEBHOOK_SECRET = 'YOUR_SECRET_HERE';

function verifySignature(payload, signature) {
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhook', (req, res) => {
  const signature = req.header('x-metaplane-sig');
  if (!signature) {
    return res.status(401).json({ error: 'No signature provided' });
  }

  // req.body is a Buffer since we used express.raw()
  if (!verifySignature(req.body, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Parse the JSON after verification
  const data = JSON.parse(req.body);
  console.log('Received verified webhook:', data);
  
  res.json({ status: 'success' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
```

Once you've verified the signature you can be sure that the request came from Metaplane. If you'd like to test your request verification code, simply use the "Verify" action on your alert destination. The verify action will send a signed sample webhook request.

## Payload

### Schema

You can expect the payload to match this schema:

```text
{
    id: string;
    labels: Array<{
        name: string;
        id: string;
    }>;
    status: "opened" | "resolved";
    updatedAt: string;
    createdAt: string;
    idempotencyId: string;
    acknowledged: boolean;
    acknowledgedAt: string | null;
    incidentUrl: string;
    affectedMonitors_v2: Array<{
        id: string;
        monitor: Array<{
            name: string | null;
            type: string;
            id: string;
            tags: Array<{
                name: string;
            }>;
            entities: Array<{
                id: string;
                connectionId: string;
                entityType: string;
                absolutePath: string;
            }>;
            owners: Array<{
                name: string;
                email: string;
                identities: Array<
                | {
                    type: "slack";
                    userId: string;
                }
                | {
                    type: "ms_teams";
                    userId: string;
                }
                | {
                    type: "dbt";
                    ownerId: string;
                }
                >;
            }>;
        }>;
        group: Array<{
            name: string;
            value: string;
        }> | null;
        linkedAt: string;
        resolvedAt: string | null;
        connections: Array<{
            name: string | null;
            type: string;
            id: string;
        }>;
    }>;
    /* affectedMonitors is deprecated. Please use affectedMonitors_v2 */
    affectedMonitors: Array<{
        id: string;
        monitor: {
            name: string | null;
            type: string;
            id: string;
            tags: Array<{
                name: string;
            }>;
            entityType: string;
            absolutePath: string;
            owners: Array<{
                name: string;
                email: string;
                identities: Array<
                | {
                    type: "slack";
                    userId: string;
                }
                | {
                    type: "ms_teams";
                    userId: string;
                }
                | {
                    type: "dbt";
                    ownerId: string;
                }
                >;
            }>;
        };
        group: Array<{
            name: string;
            value: string;
        }> | null;
        linkedAt: string;
        resolvedAt: string | null;
        connections: Array<{
            name: string | null;
            type: string;
            id: string;
        }>;
    }>;
};
```

### Delivery semantics

The webhook systems conforms to at-least-once delivery semantics. This effectively means that you may receive the exact same event more than once — your 3rd party service should be idempotent so that this detail doesn't cause issues.

One straight-forward way of identifying duplicate payloads is by using the `idempotencyId`. Multiple payloads with the same `idempotencyId` will be identical. However, multiple payloads with different `idempotencyId`s aren't guaranteed to be different. That is, it's possible that 2 payloads have different `idempotencyId`s while the the rest of the payload content is the same.

### `affectedMonitors`

*`affectedMonitors`is now deprecated. Please use`affectedMonitors_V2`*

The items within `affectedMonitors_V2` represent monitors that have been implicated in the incident. Currently, you can expect that only monitors are affected.

Affected monitors may have a `group` value. If the `group` value is not `null` then it'll be an array of key-value pairs that identify a specific group. The `group` property will only be populated if the alerting monitor is a [Group By monitor](https://dash.readme.com/project/metaplane/v1.0/docs/group-by-monitors).

### `acknowledged` and `acknowledgedAt`

As of September 16th, 2024 the `acknowledged` and `acknowledgedAt` fields are deprecated. While the payload fields will continue to exist for backward compatibility, there is no longer a way to affect the acknowledged state of an incident.

Instead, the `labels` field has superseded acknowledge functionality.

### Example

```json
{
  "id": "25674",
  "createdAt": "2024-03-18T14:10:56.757Z",
  "updatedAt": "2024-09-30T18:22:07.603Z",
  "idempotencyId": "MjU2NzQtMjAyNC0wOS0zMFQxODoyMjowNy42MDNa",
  "status": "opened",
  "acknowledged": false,
  "acknowledgedAt": null,
  "incidentUrl": "https://app.metaplane.dev/incident/25674",
  "affectedMonitors_v2": [
    {
      "id": "26084",
      "linkedAt": "2024-03-18T14:11:08.300Z",
      "resolvedAt": null,
      "monitor": {
        "id": "018e51e4-8ed9-7bc3-9aee-3d6011375577",
        "type": "CUSTOM",
        "name": "Group Monitor",
        "entities": [
          {
            "id": "1803f576-2b43-4f4b-b7a5-8d66a898d063",
            "connectionId": "a903aa1f-67d2-46c4-8a69-4553d3709151",
            "entityType": "database",
            "absolutePath": "DEMO_DB"
          }
        ],
        "owners": [
          {
            "name": "John Smith",
            "email": "j@metaplane.dev",
            "identities": [{ "type": "slack", "userId": "U07126QAF4X" }]
          }
        ],
        "tags": [{ "name": "dbt seed" }]
      },
      "group": [
        { "name": "group_id", "value": "7c70f325-80e2-4cda-8e06-104dc0a78270" }
      ],
      "connections": [
        {
          "id": "a903aa1f-67d2-46c4-8a69-4553d3709151",
          "type": "snowflake",
          "name": "Snowflake DB"
        }
      ]
    }
  ],
  "affectedMonitors": [
    {
      "id": "26084",
      "linkedAt": "2024-03-18T14:11:08.300Z",
      "resolvedAt": null,
      "monitor": {
        "id": "018e51e4-8ed9-7bc3-9aee-3d6011375577",
        "type": "CUSTOM",
        "name": "Group Monitor",
        "entityType": "database",
        "absolutePath": "DEMO_DB",
        "owners": [
          {
            "name": "John Smith",
            "email": "j@metaplane.dev",
            "identities": [{ "type": "slack", "userId": "U07126QAF4X" }]
          }
        ],
        "tags": [{ "name": "dbt seed" }]
      },
      "group": [
        { "name": "group_id", "value": "7c70f325-80e2-4cda-8e06-104dc0a78270" }
      ],
      "connections": [
        {
          "id": "a903aa1f-67d2-46c4-8a69-4553d3709151",
          "type": "snowflake",
          "name": "Snowflake DB"
        }
      ]
    }
  ],
  "labels": []
}

```