# Inbound Webhooks

Create or update events from signed inbound HTTP requests.

Source: https://statusdashboard.com/docs/integrations/webhooks/inbound

> This is a plan-gated feature. If it isn't available in your account, visit the [Billing](/docs/org-mgmt/billing) page or contact support to review your options.

Inbound webhooks let external systems create and update **incident** events on your status dashboards without manual entry in the admin console. Monitoring tools, CI pipelines, and incident platforms send signed HTTPS POST requests; StatusDashboard validates the signature and queues the payload for processing.

Create signing keys on [Inbound webhooks](/app/integrations/webhooks/inbound). The sections below cover authentication, trigger and update payloads, idempotency, code examples, and webhook logs.

***

## Endpoint
Inbound webhooks let external tools automatically open and update **incident** events on your StatusDashboard pages. No polling, no manual entry, and no third-party integration middleware required.

> Inbound webhooks support 
>   **incidents only**
>   . Maintenance windows and informational notices should be created via the 
>   [REST API](/docs/api)
>    or the admin console.

| Method | Endpoint                                                  |
| ------ | --------------------------------------------------------- |
| `POST` | `https://api.statusdashboard.com/public/webhooks/inbound` |

## Authentication
Every request must include a valid signing key. Signing keys are created on [Inbound webhooks](/app/integrations/webhooks/inbound). Each key has a **Key ID** and a **Signing Secret**.

**The signing secret is shown exactly once when the key is created.** Store it securely in your secrets manager — it cannot be retrieved after the creation dialog is closed.

### Request headers
| Header           | Value                                            |
| ---------------- | ------------------------------------------------ |
| `Content-Type`   | `application/json`                               |
| `X-SD-Key-Id`    | Your signing key ID (e.g. `wk_a1b2c3d4e5f6a7b8`) |
| `X-SD-Signature` | HMAC-SHA256 signature (see format below)         |

### Signature format
The `X-SD-Signature` header must be formatted as:

```
t=<unix_timestamp>,v1=<hmac_hex>
```

Where:

* `<unix_timestamp>` is the current Unix timestamp in **seconds** (e.g. `1712345678`)
* `<hmac_hex>` is an HMAC-SHA256 hex digest computed over the string `<unix_timestamp>.<raw_request_body>`, using your signing secret as the key

StatusDashboard validates the signature and rejects requests with a timestamp older than **5 minutes** to prevent replay attacks.

> All authentication failures return 
>   `200 { "ok": true }`
>   . This is intentional — returning 4xx on auth failure would allow an attacker to enumerate whether a Key ID exists. If events aren't appearing, verify that the Key ID and signing secret match what is shown in the dashboard.

## Payload reference
The request body must be a JSON object. The `action` field determines which operation is performed.

### Trigger action — create a new incident
Use `action: "trigger"` to open a new incident event.

| Field                            | Type                       | Required | Description                                                                                                                                                                                              |
| -------------------------------- | -------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action`                         | `"trigger"`                | Yes      | Identifies this as a trigger operation                                                                                                                                                                   |
| `title`                          | string                     | Yes      | Incident title (1–250 characters)                                                                                                                                                                        |
| `description`                    | string                     | Yes      | Incident description (1–5000 characters)                                                                                                                                                                 |
| `impactAnalysis`                 | string                     | No       | Optional customer and service impact details. Rich text field. See [Rich text fields](/docs/api#rich-text-fields). Max 5,000 characters.                                                                 |
| `severity`                       | string                     | Yes      | Event-level severity label (must match a configured severity, e.g. `"Major Outage"`). Applied uniformly to every component in `componentIds`.                                                            |
| `componentIds`                   | string\[]                  | Yes      | UUIDs of the affected components (1–20 IDs). Obtain IDs from `GET /app/components`.                                                                                                                      |
| `statusLabel`                    | string                     | No       | Workflow phase label to start at (e.g. `"Investigating"`). Defaults to the first non-final incident phase.                                                                                               |
| `initialMessage`                 | string                     | No       | First timeline message (1–5000 characters). Defaults to `"Incident opened via inbound webhook."` when omitted.                                                                                           |
| `idempotencyKey`                 | string                     | No       | Optional dedupe key for this trigger (1–128 characters). Prevents duplicate incidents when your tool retries the same delivery. See [Avoiding duplicate incidents](#avoiding-duplicate-incidents) below. |
| `notifications`                  | boolean                    | No       | Whether to send subscriber email notifications when the incident is created. Defaults to `true`.                                                                                                         |
| `segmentNotification`            | object                     | No       | Target notifications to specific [subscriber segments](/docs/\(platform\)/subscribers/segments). Requires the **segments** feature on your plan.                                                         |
| `segmentNotification.enabled`    | boolean                    | —        | When `true`, applies segment filtering to notification recipients.                                                                                                                                       |
| `segmentNotification.mode`       | `"include"` \| `"exclude"` | —        | `include` sends only to subscribers in the selected segments; `exclude` suppresses them.                                                                                                                 |
| `segmentNotification.segmentIds` | string\[] (UUID)           | —        | Segment IDs from `GET /app/segments`. At least one required when `enabled` is `true` and `mode` is `include`.                                                                                            |
| `attributes`                     | array                      | No       | Custom key/value attributes on the event (max count per plan). Each item: `{ "key": string, "value": string, "isPublic": boolean }`.                                                                     |

> Unlike event creation via the admin console or REST API, inbound webhooks use a single 
>   `severity`
>    for the whole incident. That severity is applied to every listed component — you cannot assign a different severity per component in one webhook payload.

> `segmentNotification`
>    requires the 
>   **segments**
>    feature. If your plan does not include segments, requests that include this field fail with a 
>   `processing_failed`
>    webhook log outcome.

### Update action — advance an existing incident
Use `action: "update"` to add a timeline entry and advance the workflow phase of an existing incident.

`impactAnalysis` is **not accepted** on update actions. Set it on trigger, or use `PATCH /app/events/{id}` to change it later.

| Field                 | Type          | Required | Description                                                                                       |
| --------------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `action`              | `"update"`    | Yes      | Identifies this as an update operation                                                            |
| `eventId`             | string (UUID) | Yes      | The ID of the event to update                                                                     |
| `statusLabel`         | string        | Yes      | The workflow phase label to advance to (e.g. `"Resolved"`)                                        |
| `message`             | string        | Yes      | The timeline message for this update (1–5000 characters)                                          |
| `notifications`       | boolean       | No       | Update whether future notifications are sent for this event.                                      |
| `segmentNotification` | object        | No       | Update segment notification targeting (same shape as trigger). Requires the **segments** feature. |
| `attributes`          | array         | No       | Replace event attributes (same shape as trigger).                                                 |

Status updates always enqueue a timeline notification when `notifications` is `true` on the event after processing.

## Response
| Status | Meaning                                                                                                                                                                  |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `200`  | Request queued for processing (also returned for authentication failures — see note above). Check webhook logs for the final `processed` or `processing_failed` outcome. |
| `400`  | Invalid request body — malformed JSON or schema validation error                                                                                                         |
| `429`  | Rate limit exceeded — slow down your request rate                                                                                                                        |

## Rate limiting
Each org has a configurable inbound webhook rate limit (requests per minute). If the limit is reached, the server returns `429`. The rate limit only applies after authentication succeeds, so a `429` confirms that the Key ID and signature were valid.

## Avoiding duplicate incidents
Monitoring tools and scripts often **retry** webhook requests when a connection times out or the response is slow. A `200` response means your request was **queued**, not that the incident already exists — so retries can accidentally open the same incident more than once if you do not deduplicate on your side.

For `action: "trigger"`, you can send an optional **idempotency key** so repeated deliveries with the same key reuse the original incident instead of creating another one.

| How to send it           | Example                                          |
| ------------------------ | ------------------------------------------------ |
| JSON field (recommended) | `"idempotencyKey": "pagerduty-incident-48291"`   |
| Request header           | `X-SD-Idempotency-Key: pagerduty-incident-48291` |

Use a value that uniquely identifies the **specific outage or alert** — for example your monitoring system's incident ID, or a hash of the alert fingerprint. If you omit a key, each accepted trigger creates a new incident (same as today).

**Good practices:**

* Generate a new key for each distinct incident; do not reuse a generic label like `"api-down"` across unrelated outages.
* Include the key in the signed JSON body so it is covered by your HMAC signature.
* If the body field and header are both sent, they must match.

Keys are remembered for **3 days**, then expire automatically. After that window, the same key can open a new incident if you send it again.

> Idempotency applies to 
>   **trigger**
>    (create) only. 
>   **Update**
>    requests are not deduplicated — use the 
>   `eventId`
>    from your first successful trigger when sending timeline updates.

## Code examples
Store your Key ID and Signing Secret in environment variables — never hard-code credentials.

Beyond the required fields, you may optionally include `notifications`, `segmentNotification`, and `attributes` on both `trigger` and `update` payloads. Omit any you do not need — defaults apply (`notifications` defaults to `true` on trigger).

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Node.js">
      Node.js
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```shell
    # Required fields only
    BODY='{"action":"trigger","title":"Database high latency","description":"Queries taking >5s","severity":"Major Outage","componentIds":["b2c3d4e5-f6a7-8901-bcde-f12345678901"]}'

    # Or include optional notification, segment, and attribute fields:
    # BODY='{"action":"trigger","title":"Database high latency","description":"Queries taking >5s","severity":"Major Outage","componentIds":["b2c3d4e5-f6a7-8901-bcde-f12345678901"],"notifications":true,"segmentNotification":{"enabled":true,"mode":"include","segmentIds":["<segment-uuid>"]},"attributes":[{"key":"runbook_url","value":"https://wiki.example.com/db-latency","isPublic":true}]}'

    TS=$(date +%s)
    SIG=$(echo -n "${TS}.${BODY}" | openssl dgst -sha256 -hmac "$SD_SIGNING_SECRET" | awk '{print $2}')

    curl -X POST https://api.statusdashboard.com/public/webhooks/inbound \
      -H "Content-Type: application/json" \
      -H "X-SD-Key-Id: $SD_KEY_ID" \
      -H "X-SD-Signature: t=${TS},v1=${SIG}" \
      -d "$BODY"
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js">
    ```javascript
    import crypto from 'crypto';

    const KEY_ID   = process.env.SD_KEY_ID;
    const SECRET   = process.env.SD_SIGNING_SECRET;
    const ENDPOINT = 'https://api.statusdashboard.com/public/webhooks/inbound';

    async function sendWebhook(payload) {
      const body = JSON.stringify(payload);
      const ts   = Math.floor(Date.now() / 1000);
      const sig  = crypto.createHmac('sha256', SECRET)
                         .update(`${ts}.${body}`)
                         .digest('hex');

      const res = await fetch(ENDPOINT, {
        method:  'POST',
        headers: {
          'Content-Type':   'application/json',
          'X-SD-Key-Id':    KEY_ID,
          'X-SD-Signature': `t=${ts},v1=${sig}`,
        },
        body,
      });
      return res.json();
    }

    // Trigger a new incident (required fields only)
    await sendWebhook({
      action:       'trigger',
      title:        'Database high latency',
      description:  'Queries taking >5 seconds.',
      severity:     'Major Outage',
      componentIds: ['b2c3d4e5-f6a7-8901-bcde-f12345678901'],
    });

    // Trigger with optional fields
    await sendWebhook({
      action:       'trigger',
      title:        'Database high latency',
      description:  'Queries taking >5 seconds.',
      severity:     'Major Outage',
      componentIds: ['b2c3d4e5-f6a7-8901-bcde-f12345678901'],
      statusLabel:  'Investigating',       // optional
      initialMessage: 'We are looking into elevated query times.', // optional
      notifications: true,                  // optional — defaults to true when omitted
      segmentNotification: {              // optional — requires segments feature on your plan
        enabled:    true,
        mode:       'include',
        segmentIds: ['<segment-uuid>'],
      },
      attributes: [                       // optional
        { key: 'runbook_url', value: 'https://wiki.example.com/db-latency', isPublic: true },
        { key: 'pagerduty_id', value: 'PABC123', isPublic: false },
      ],
    });

    // Update an existing incident (required fields only)
    await sendWebhook({
      action:      'update',
      eventId:     'f47ac10b-58cc-4372-a567-0e02b2c3d482',
      statusLabel: 'Resolved',
      message:     'Latency has returned to normal. Monitoring.',
    });

    // Update with optional metadata fields
    await sendWebhook({
      action:      'update',
      eventId:     'f47ac10b-58cc-4372-a567-0e02b2c3d482',
      statusLabel: 'Resolved',
      message:     'Latency has returned to normal. Monitoring.',
      notifications: false,               // optional — disable future notifications on this event
      attributes: [                     // optional — replaces event attributes
        { key: 'resolved_by', value: 'on-call-automation', isPublic: true },
      ],
    });
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import hmac, hashlib, time, json, os
    import urllib.request

    KEY_ID   = os.environ['SD_KEY_ID']
    SECRET   = os.environ['SD_SIGNING_SECRET'].encode()
    ENDPOINT = 'https://api.statusdashboard.com/public/webhooks/inbound'

    def send_webhook(payload: dict) -> dict:
        body = json.dumps(payload).encode()
        ts   = str(int(time.time()))
        sig  = hmac.new(SECRET, f'{ts}.'.encode() + body, hashlib.sha256).hexdigest()

        req = urllib.request.Request(
            ENDPOINT,
            data    = body,
            method  = 'POST',
            headers = {
                'Content-Type':   'application/json',
                'X-SD-Key-Id':    KEY_ID,
                'X-SD-Signature': f't={ts},v1={sig}',
            },
        )
        with urllib.request.urlopen(req) as r:
            return json.loads(r.read())

    # Trigger a new incident (required fields only)
    send_webhook({
        'action':       'trigger',
        'title':        'Database high latency',
        'description':  'Queries taking >5 seconds.',
        'severity':     'Major Outage',
        'componentIds': ['b2c3d4e5-f6a7-8901-bcde-f12345678901'],
    })

    # Trigger with optional fields
    send_webhook({
        'action':       'trigger',
        'title':        'Database high latency',
        'description':  'Queries taking >5 seconds.',
        'severity':     'Major Outage',
        'componentIds': ['b2c3d4e5-f6a7-8901-bcde-f12345678901'],
        'statusLabel':  'Investigating',       # optional
        'initialMessage': 'We are looking into elevated query times.',  # optional
        'notifications': True,                 # optional — defaults to True when omitted
        'segmentNotification': {               # optional — requires segments feature on your plan
            'enabled':    True,
            'mode':       'include',
            'segmentIds': ['<segment-uuid>'],
        },
        'attributes': [                        # optional
            {'key': 'runbook_url', 'value': 'https://wiki.example.com/db-latency', 'isPublic': True},
            {'key': 'pagerduty_id', 'value': 'PABC123', 'isPublic': False},
        ],
    })

    # Update an existing incident (required fields only)
    send_webhook({
        'action':      'update',
        'eventId':     'f47ac10b-58cc-4372-a567-0e02b2c3d482',
        'statusLabel': 'Resolved',
        'message':     'Latency has returned to normal.',
    })

    # Update with optional metadata fields
    send_webhook({
        'action':      'update',
        'eventId':     'f47ac10b-58cc-4372-a567-0e02b2c3d482',
        'statusLabel': 'Resolved',
        'message':     'Latency has returned to normal. Monitoring.',
        'notifications': False,              # optional — disable future notifications on this event
        'attributes': [                      # optional — replaces event attributes
            {'key': 'resolved_by', 'value': 'on-call-automation', 'isPublic': True},
        ],
    })
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Webhook logs
StatusDashboard records an entry for every **authenticated** inbound webhook request. Logs appear below the signing keys on the inbound webhooks page (10 entries per page; use Prev/Next to browse older entries).

### What is logged
| Outcome             | When                                                                                                                                               |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accepted`          | The payload passed schema validation and was queued for async processing                                                                           |
| `processed`         | The worker successfully created or updated an incident                                                                                             |
| `processing_failed` | The payload was valid JSON but failed semantic validation (unknown severity, component, or workflow phase; plan limits; non-incident update; etc.) |
| `rate_limited`      | Authentication succeeded but the org rate limit was exceeded                                                                                       |
| `validation_failed` | Authentication succeeded but the payload failed JSON parsing or schema validation                                                                  |

Authentication failures (bad Key ID, bad signature, stale timestamp) are **not** logged — the Key ID must be valid and the signature must pass before a log entry is written.

> A 
>   `200 { "ok": true }`
>    response means the request was 
>   **queued**
>   , not that an event was created. Always check webhook logs for the terminal 
>   `processed`
>    or 
>   `processing_failed`
>    outcome.

### Log details
Each log entry shows:

* **Outcome** — `accepted`, `processed`, `processing_failed`, `rate_limited`, or `validation_failed`
* **Action** — the `action` field from the payload (e.g. `trigger`, `update`), or `unknown` if the payload could not be parsed
* **Key ID** — which signing key was used
* **Received at** — timestamp of the request
* **Reason** — for failures, a short description of why processing failed
* **Event ID** — present on `processed` outcomes; links to the created or updated incident
* **Payload** — the raw request body, up to 16 KB

Click any log row to open the detail view, which shows the full parsed payload and a copy button.

> If a payload exceeds 16 KB, it is truncated at that boundary and the detail view displays a warning. Outcomes reflect the full, untruncated payload — truncation only affects what is stored for inspection.

### Retention
Webhook logs are retained for **30 days** and then automatically deleted. They are not part of your audit log.
